Fix connected account permissions (#17598)
Fixes #17411 --------- Co-authored-by: neo773 <neo773@protonmail.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -415,28 +415,39 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
element={<SettingsTwoFactorAuthenticationMethod />}
|
||||
/>
|
||||
<Route path={SettingsPath.Experience} element={<SettingsExperience />} />
|
||||
<Route path={SettingsPath.Accounts} element={<SettingsAccounts />} />
|
||||
<Route path={SettingsPath.NewAccount} element={<SettingsNewAccount />} />
|
||||
<Route
|
||||
path={SettingsPath.AccountsConfiguration}
|
||||
element={<SettingsAccountsConfiguration />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AccountsCalendars}
|
||||
element={<SettingsAccountsCalendars />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AccountsEmails}
|
||||
element={<SettingsAccountsEmails />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.NewImapSmtpCaldavConnection}
|
||||
element={<SettingsNewImapSmtpCaldavConnection />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.EditImapSmtpCaldavConnection}
|
||||
element={<SettingsEditImapSmtpCaldavConnection />}
|
||||
/>
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
settingsPermission={PermissionFlagType.CONNECTED_ACCOUNTS}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Route path={SettingsPath.Accounts} element={<SettingsAccounts />} />
|
||||
<Route
|
||||
path={SettingsPath.NewAccount}
|
||||
element={<SettingsNewAccount />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AccountsConfiguration}
|
||||
element={<SettingsAccountsConfiguration />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AccountsCalendars}
|
||||
element={<SettingsAccountsCalendars />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AccountsEmails}
|
||||
element={<SettingsAccountsEmails />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.NewImapSmtpCaldavConnection}
|
||||
element={<SettingsNewImapSmtpCaldavConnection />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.EditImapSmtpCaldavConnection}
|
||||
element={<SettingsEditImapSmtpCaldavConnection />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
element={
|
||||
<SettingsProtectedRouteWrapper
|
||||
|
||||
+19
-6
@@ -1,16 +1,22 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
|
||||
import { InformationBannerBillingSubscriptionPaused } from '@/information-banner/components/billing/InformationBannerBillingSubscriptionPaused';
|
||||
import { InformationBannerEndTrialPeriod } from '@/information-banner/components/billing/InformationBannerEndTrialPeriod';
|
||||
import { InformationBannerFailPaymentInfo } from '@/information-banner/components/billing/InformationBannerFailPaymentInfo';
|
||||
import { InformationBannerNoBillingSubscription } from '@/information-banner/components/billing/InformationBannerNoBillingSubscription';
|
||||
import { InformationBannerReconnectAccountEmailAliases } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases';
|
||||
import { InformationBannerReconnectAccountInsufficientPermissions } from '@/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { useIsSomeMeteredProductCapReached } from '@/workspace/hooks/useIsSomeMeteredProductCapReached';
|
||||
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
|
||||
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { SubscriptionStatus } from '~/generated-metadata/graphql';
|
||||
|
||||
import {
|
||||
PermissionFlagType,
|
||||
SubscriptionStatus,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledInformationBannerWrapper = styled.div`
|
||||
position: relative;
|
||||
@@ -22,6 +28,9 @@ const StyledInformationBannerWrapper = styled.div`
|
||||
|
||||
export const InformationBannerWrapper = () => {
|
||||
const subscriptionStatus = useSubscriptionStatus();
|
||||
const permissionMap = usePermissionFlagMap();
|
||||
const isAccountSyncEnabled =
|
||||
permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS];
|
||||
const isWorkspaceSuspended = useIsWorkspaceActivationStatusEqualsTo(
|
||||
WorkspaceActivationStatus.SUSPENDED,
|
||||
);
|
||||
@@ -43,8 +52,12 @@ export const InformationBannerWrapper = () => {
|
||||
|
||||
return (
|
||||
<StyledInformationBannerWrapper>
|
||||
<InformationBannerReconnectAccountInsufficientPermissions />
|
||||
<InformationBannerReconnectAccountEmailAliases />
|
||||
{isAccountSyncEnabled && (
|
||||
<InformationBannerReconnectAccountInsufficientPermissions />
|
||||
)}
|
||||
{isAccountSyncEnabled && (
|
||||
<InformationBannerReconnectAccountEmailAliases />
|
||||
)}
|
||||
{displayBillingSubscriptionPausedBanner && (
|
||||
<InformationBannerBillingSubscriptionPaused /> // TODO: remove this once paused subscriptions are deprecated
|
||||
)}
|
||||
|
||||
+22
@@ -3,10 +3,13 @@ import { RecoilRoot, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
|
||||
|
||||
import {
|
||||
OnboardingStatus,
|
||||
PermissionFlagType,
|
||||
SubscriptionStatus,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import {
|
||||
@@ -18,16 +21,21 @@ const renderHooks = (
|
||||
onboardingStatus: OnboardingStatus,
|
||||
withCurrentBillingSubscription: boolean,
|
||||
withOneWorkspaceMember = true,
|
||||
permissionFlags = mockedUserData.currentUserWorkspace.permissionFlags,
|
||||
) => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const [currentUser, setCurrentUser] = useRecoilState(currentUserState);
|
||||
const setCurrentUserWorkspace = useSetRecoilState(
|
||||
currentUserWorkspaceState,
|
||||
);
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
const setNextOnboardingStatus = useSetNextOnboardingStatus();
|
||||
return {
|
||||
currentUser,
|
||||
setCurrentUser,
|
||||
setCurrentWorkspace,
|
||||
setCurrentUserWorkspace,
|
||||
setNextOnboardingStatus,
|
||||
};
|
||||
},
|
||||
@@ -37,6 +45,10 @@ const renderHooks = (
|
||||
);
|
||||
act(() => {
|
||||
result.current.setCurrentUser({ ...mockedUserData, onboardingStatus });
|
||||
result.current.setCurrentUserWorkspace({
|
||||
...mockedUserData.currentUserWorkspace,
|
||||
permissionFlags,
|
||||
});
|
||||
result.current.setCurrentWorkspace({
|
||||
...mockCurrentWorkspace,
|
||||
currentBillingSubscription: withCurrentBillingSubscription
|
||||
@@ -75,6 +87,16 @@ describe('useSetNextOnboardingStatus', () => {
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.COMPLETED);
|
||||
});
|
||||
|
||||
it('should skip SyncEmail when account sync is disabled', () => {
|
||||
const nextOnboardingStatus = renderHooks(
|
||||
OnboardingStatus.PROFILE_CREATION,
|
||||
false,
|
||||
true,
|
||||
[PermissionFlagType.WORKSPACE_MEMBERS],
|
||||
);
|
||||
expect(nextOnboardingStatus).toEqual(OnboardingStatus.INVITE_TEAM);
|
||||
});
|
||||
|
||||
it('should set next onboarding status for SyncEmail', () => {
|
||||
const nextOnboardingStatus = renderHooks(
|
||||
OnboardingStatus.SYNC_EMAIL,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type CurrentUser,
|
||||
currentUserState,
|
||||
@@ -9,21 +11,36 @@ import {
|
||||
currentWorkspaceState,
|
||||
} from '@/auth/states/currentWorkspaceState';
|
||||
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { OnboardingStatus } from '~/generated-metadata/graphql';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
|
||||
const getNextOnboardingStatus = (
|
||||
currentUser: CurrentUser | null,
|
||||
currentWorkspace: CurrentWorkspace | null,
|
||||
calendarBookingPageId: string | null,
|
||||
) => {
|
||||
import {
|
||||
OnboardingStatus,
|
||||
PermissionFlagType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type GetNextOnboardingStatusArgs = {
|
||||
currentUser: CurrentUser | null;
|
||||
currentWorkspace: CurrentWorkspace | null;
|
||||
calendarBookingPageId: string | null;
|
||||
isAccountSyncEnabled: boolean;
|
||||
};
|
||||
|
||||
const getNextOnboardingStatus = ({
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
calendarBookingPageId,
|
||||
isAccountSyncEnabled,
|
||||
}: GetNextOnboardingStatusArgs) => {
|
||||
if (currentUser?.onboardingStatus === OnboardingStatus.WORKSPACE_ACTIVATION) {
|
||||
return OnboardingStatus.PROFILE_CREATION;
|
||||
}
|
||||
|
||||
if (currentUser?.onboardingStatus === OnboardingStatus.PROFILE_CREATION) {
|
||||
if (currentWorkspace?.workspaceMembersCount === 1) {
|
||||
return OnboardingStatus.SYNC_EMAIL;
|
||||
if (isAccountSyncEnabled) {
|
||||
return OnboardingStatus.SYNC_EMAIL;
|
||||
}
|
||||
return OnboardingStatus.INVITE_TEAM;
|
||||
}
|
||||
return OnboardingStatus.COMPLETED;
|
||||
}
|
||||
@@ -48,15 +65,19 @@ export const useSetNextOnboardingStatus = () => {
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const calendarBookingPageId = useRecoilValue(calendarBookingPageIdState);
|
||||
const permissionMap = usePermissionFlagMap();
|
||||
const isAccountSyncEnabled =
|
||||
permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS];
|
||||
|
||||
return useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
const nextOnboardingStatus = getNextOnboardingStatus(
|
||||
const nextOnboardingStatus = getNextOnboardingStatus({
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
calendarBookingPageId,
|
||||
);
|
||||
isAccountSyncEnabled,
|
||||
});
|
||||
set(currentUserState, (current) => {
|
||||
if (isDefined(current)) {
|
||||
return {
|
||||
@@ -67,6 +88,11 @@ export const useSetNextOnboardingStatus = () => {
|
||||
return current;
|
||||
});
|
||||
},
|
||||
[currentWorkspace, currentUser, calendarBookingPageId],
|
||||
[
|
||||
currentUser,
|
||||
currentWorkspace,
|
||||
calendarBookingPageId,
|
||||
isAccountSyncEnabled,
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
+21
-2
@@ -4,6 +4,7 @@ import { renderHook } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { type MutableSnapshot, RecoilRoot } from 'recoil';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
type Billing,
|
||||
OnboardingStatus,
|
||||
@@ -78,6 +79,7 @@ describe('useSettingsNavigationItems', () => {
|
||||
[PermissionFlagType.API_KEYS_AND_WEBHOOKS]: false,
|
||||
[PermissionFlagType.ROLES]: false,
|
||||
[PermissionFlagType.SECURITY]: false,
|
||||
[PermissionFlagType.CONNECTED_ACCOUNTS]: false,
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useSettingsNavigationItems(), {
|
||||
@@ -99,6 +101,7 @@ describe('useSettingsNavigationItems', () => {
|
||||
[PermissionFlagType.API_KEYS_AND_WEBHOOKS]: true,
|
||||
[PermissionFlagType.ROLES]: true,
|
||||
[PermissionFlagType.SECURITY]: true,
|
||||
[PermissionFlagType.CONNECTED_ACCOUNTS]: true,
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useSettingsNavigationItems(), {
|
||||
@@ -113,6 +116,16 @@ describe('useSettingsNavigationItems', () => {
|
||||
});
|
||||
|
||||
it('should show user section items regardless of permissions', () => {
|
||||
(usePermissionFlagMap as jest.Mock).mockImplementation(() => ({
|
||||
[PermissionFlagType.WORKSPACE]: false,
|
||||
[PermissionFlagType.WORKSPACE_MEMBERS]: false,
|
||||
[PermissionFlagType.DATA_MODEL]: false,
|
||||
[PermissionFlagType.API_KEYS_AND_WEBHOOKS]: false,
|
||||
[PermissionFlagType.ROLES]: false,
|
||||
[PermissionFlagType.SECURITY]: false,
|
||||
[PermissionFlagType.CONNECTED_ACCOUNTS]: false,
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useSettingsNavigationItems(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
@@ -120,7 +133,13 @@ describe('useSettingsNavigationItems', () => {
|
||||
const userSection = result.current.find(
|
||||
(section) => section.label === 'User',
|
||||
);
|
||||
expect(userSection?.items.length).toBeGreaterThan(0);
|
||||
expect(userSection?.items.every((item) => !item.isHidden)).toBe(true);
|
||||
expect(
|
||||
userSection?.items.filter((item) => !item.isHidden).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
userSection?.items
|
||||
.filter((item) => item.path !== SettingsPath.Accounts)
|
||||
.every((item) => !item.isHidden),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,17 +99,20 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
||||
label: t`Accounts`,
|
||||
path: SettingsPath.Accounts,
|
||||
Icon: IconAt,
|
||||
isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
|
||||
subItems: [
|
||||
{
|
||||
label: t`Emails`,
|
||||
path: SettingsPath.AccountsEmails,
|
||||
Icon: IconMail,
|
||||
isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
|
||||
indentationLevel: 2,
|
||||
},
|
||||
{
|
||||
label: t`Calendars`,
|
||||
path: SettingsPath.AccountsCalendars,
|
||||
Icon: IconCalendarEvent,
|
||||
isHidden: !permissionMap[PermissionFlagType.CONNECTED_ACCOUNTS],
|
||||
indentationLevel: 2,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -192,7 +192,10 @@ export const mockedUserData: MockedUser = {
|
||||
workspaceMember: mockedWorkspaceMemberData,
|
||||
currentWorkspace: mockCurrentWorkspace,
|
||||
currentUserWorkspace: {
|
||||
permissionFlags: [PermissionFlagType.WORKSPACE_MEMBERS],
|
||||
permissionFlags: [
|
||||
PermissionFlagType.WORKSPACE_MEMBERS,
|
||||
PermissionFlagType.CONNECTED_ACCOUNTS,
|
||||
],
|
||||
twoFactorAuthenticationMethodSummary: [],
|
||||
objectsPermissions: generatedMockObjectMetadataItems.map((item) => ({
|
||||
objectMetadataId: item.id,
|
||||
|
||||
+8
-3
@@ -24,7 +24,6 @@ import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services
|
||||
@MetadataResolver()
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@UseFilters(AuthGraphqlApiExceptionFilter, PermissionsGraphqlApiExceptionFilter)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKSPACE))
|
||||
export class ImapSmtpCaldavResolver {
|
||||
constructor(
|
||||
private readonly ImapSmtpCaldavConnectionService: ImapSmtpCaldavService,
|
||||
@@ -33,7 +32,10 @@ export class ImapSmtpCaldavResolver {
|
||||
) {}
|
||||
|
||||
@Query(() => ConnectedImapSmtpCaldavAccountDTO)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS),
|
||||
)
|
||||
async getConnectedImapSmtpCaldavAccount(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -60,7 +62,10 @@ export class ImapSmtpCaldavResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => ImapSmtpCaldavConnectionSuccessDTO)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS),
|
||||
)
|
||||
async saveImapSmtpCaldavAccount(
|
||||
@Args('accountOwnerId', { type: () => UUIDScalarType })
|
||||
accountOwnerId: string,
|
||||
|
||||
+9
-1
@@ -6,9 +6,17 @@ import { TimelineMessagingResolver } from 'src/engine/core-modules/messaging/tim
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule, UserModule, ConnectedAccountModule],
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
UserModule,
|
||||
ConnectedAccountModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
],
|
||||
exports: [],
|
||||
providers: [
|
||||
TimelineMessagingResolver,
|
||||
|
||||
+3
@@ -2,6 +2,7 @@ import { UseGuards } from '@nestjs/common';
|
||||
import { Args, ArgsType, Field, Int, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { Max } from 'class-validator';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { CoreResolver } from 'src/engine/api/graphql/graphql-config/decorators/core-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
@@ -16,6 +17,7 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
|
||||
@@ -149,6 +151,7 @@ export class TimelineMessagingResolver {
|
||||
return timelineThreads;
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@Mutation(() => Boolean)
|
||||
async dismissReconnectAccountBanner(
|
||||
@AuthUser() user: UserEntity,
|
||||
|
||||
Reference in New Issue
Block a user