From fbd24b796e56da9894051ea54e67ad53fa2ccdb9 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Wed, 8 Jan 2025 12:32:06 +0100 Subject: [PATCH 1/8] feat(auth, settings): enhance multi-workspace handling Added support for multi-workspace checks during impersonation and streamlined token verification flows. Adjusted session clearing and navigation logic to improve user transition between workspaces. --- .../modules/auth/components/VerifyEffect.tsx | 27 ++++++-------- .../src/modules/auth/hooks/useAuth.ts | 37 ++++--------------- .../admin-panel/hooks/useImpersonate.ts | 21 ++++++++++- 3 files changed, 38 insertions(+), 47 deletions(-) diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx index 6530532fae8f..a6230e9ff206 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx @@ -27,22 +27,17 @@ export const VerifyEffect = () => { ); useEffect(() => { - const getTokens = async () => { - if (isDefined(errorMessage)) { - enqueueSnackBar(errorMessage, { - variant: SnackBarVariant.Error, - }); - } - if (!loginToken) { - navigate(AppPath.SignInUp); - } else { - setIsAppWaitingForFreshObjectMetadata(true); - await verify(loginToken); - } - }; - - if (!isLogged) { - getTokens(); + if (isDefined(errorMessage)) { + enqueueSnackBar(errorMessage, { + variant: SnackBarVariant.Error, + }); + } + + if (isDefined(loginToken)) { + setIsAppWaitingForFreshObjectMetadata(true); + verify(loginToken); + } else if (!isLogged) { + navigate(AppPath.SignInUp); } // Verify only needs to run once at mount // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index d28d49d59e06..d583e815d8eb 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -268,6 +268,8 @@ export const useAuth = () => { const handleVerify = useCallback( async (loginToken: string) => { + setIsVerifyPendingState(true); + const verifyResult = await verify({ variables: { loginToken }, }); @@ -282,16 +284,11 @@ export const useAuth = () => { setTokenPair(verifyResult.data?.verify.tokens); - const { user, workspaceMember, workspace } = await loadCurrentUser(); + await loadCurrentUser(); - return { - user, - workspaceMember, - workspace, - tokens: verifyResult.data?.verify.tokens, - }; + setIsVerifyPendingState(false); }, - [verify, setTokenPair, loadCurrentUser], + [setIsVerifyPendingState, verify, setTokenPair, loadCurrentUser], ); const handleCrendentialsSignIn = useCallback( @@ -301,21 +298,9 @@ export const useAuth = () => { password, captchaToken, ); - setIsVerifyPendingState(true); - - const { user, workspaceMember, workspace } = await handleVerify( - loginToken.token, - ); - - setIsVerifyPendingState(false); - - return { - user, - workspaceMember, - workspace, - }; + await handleVerify(loginToken.token); }, - [handleChallenge, handleVerify, setIsVerifyPendingState], + [handleChallenge, handleVerify], ); const handleSignOut = useCallback(async () => { @@ -360,13 +345,7 @@ export const useAuth = () => { ); } - const { user, workspace, workspaceMember } = await handleVerify( - signUpResult.data?.signUp.loginToken.token, - ); - - setIsVerifyPendingState(false); - - return { user, workspaceMember, workspace }; + await handleVerify(signUpResult.data?.signUp.loginToken.token); }, [ setIsVerifyPendingState, diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts index 8c08f657d9b3..3897e0589bd5 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts @@ -1,15 +1,20 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; import { useState } from 'react'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { useImpersonateMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; +import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; +import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl'; +import { useAuth } from '@/auth/hooks/useAuth'; export const useImpersonate = () => { const [currentUser] = useRecoilState(currentUserState); const [impersonate] = useImpersonateMutation(); - + const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); + const { buildWorkspaceUrl } = useBuildWorkspaceUrl(); + const { clearSession } = useAuth(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const [isLoading, setIsLoading] = useState(false); @@ -39,6 +44,18 @@ export const useImpersonate = () => { const { loginToken, workspace } = impersonateResult.data.impersonate; + if (!isMultiWorkspaceEnabled) { + await clearSession(); + + return (window.location.href = buildWorkspaceUrl( + undefined, + AppPath.Verify, + { + loginToken: loginToken.token, + }, + )); + } + return redirectToWorkspaceDomain(workspace.subdomain, AppPath.Verify, { loginToken: loginToken.token, }); From 3f56143b748b6d05a3bbc54ed2089ee0f52d253d Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Wed, 8 Jan 2025 15:13:22 +0100 Subject: [PATCH 2/8] refactor(auth, settings): simplify test assertions and update impersonation flow Simplified test assertions in `useAuth.test.tsx` by removing redundant property checks from the signup method. Enhanced `useImpersonate` hook by refactoring workspace condition logic and introducing fresh metadata states to improve session handling. --- .../auth/hooks/__tests__/useAuth.test.tsx | 5 +-- .../admin-panel/hooks/useImpersonate.ts | 31 +++++++++---------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx index 6430246e359f..99bea99254f8 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useAuth.test.tsx @@ -141,10 +141,7 @@ describe('useAuth', () => { const { result } = renderHooks(); await act(async () => { - const res = await result.current.signUpWithCredentials(email, password); - expect(res).toHaveProperty('user'); - expect(res).toHaveProperty('workspaceMember'); - expect(res).toHaveProperty('workspace'); + await result.current.signUpWithCredentials(email, password); }); expect(mocks[2].result).toHaveBeenCalled(); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts index 3897e0589bd5..326bcfeeccd9 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts @@ -1,20 +1,24 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; import { useState } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useImpersonateMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; -import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; -import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl'; import { useAuth } from '@/auth/hooks/useAuth'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; export const useImpersonate = () => { const [currentUser] = useRecoilState(currentUserState); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState( + isAppWaitingForFreshObjectMetadataState, + ); + + const { verify } = useAuth(); + const [impersonate] = useImpersonateMutation(); - const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); - const { buildWorkspaceUrl } = useBuildWorkspaceUrl(); - const { clearSession } = useAuth(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const [isLoading, setIsLoading] = useState(false); @@ -44,16 +48,11 @@ export const useImpersonate = () => { const { loginToken, workspace } = impersonateResult.data.impersonate; - if (!isMultiWorkspaceEnabled) { - await clearSession(); - - return (window.location.href = buildWorkspaceUrl( - undefined, - AppPath.Verify, - { - loginToken: loginToken.token, - }, - )); + if (workspace.id === currentWorkspace?.id) { + setIsAppWaitingForFreshObjectMetadata(true); + await verify(loginToken.token); + setIsAppWaitingForFreshObjectMetadata(false); + return; } return redirectToWorkspaceDomain(workspace.subdomain, AppPath.Verify, { From 669f87600fb7f6db73957aa3af19eb909596cbe8 Mon Sep 17 00:00:00 2001 From: ehconitin Date: Thu, 9 Jan 2025 16:49:34 +0530 Subject: [PATCH 3/8] wip --- .../twenty-front/src/generated/graphql.tsx | 48 +++++++++++- .../components/SettingsAdminContent.tsx | 78 ++++++++++++------- .../getFeatureFlagManagementCapability.ts | 7 ++ .../useFeatureFlagManagementCapability.ts | 11 +++ .../admin-panel/admin-panel.resolver.ts | 12 ++- .../admin-panel/admin-panel.service.ts | 36 ++++++--- .../admin-panel/dtos/user-lookup.entity.ts | 15 +++- 7 files changed, 158 insertions(+), 49 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getFeatureFlagManagementCapability.ts create mode 100644 packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagManagementCapability.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index ecc79a5e5a35..8fd9c2c24cde 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -923,6 +923,7 @@ export type Query = { findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; getAvailablePackages: Scalars['JSON']; + getFeatureFlagManagementCapability: Scalars['Boolean']; getPostgresCredentials?: Maybe; getProductPrices: ProductPricesEntity; getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput; @@ -1568,10 +1569,16 @@ export type WorkspaceEdge = { node: Workspace; }; +export type WorkspaceFeatureFlag = { + __typename?: 'WorkspaceFeatureFlag'; + key: Scalars['String']; + value: Scalars['Boolean']; +}; + export type WorkspaceInfo = { __typename?: 'WorkspaceInfo'; allowImpersonation: Scalars['Boolean']; - featureFlags: Array; + featureFlags: Array; id: Scalars['String']; logo?: Maybe; name: Scalars['String']; @@ -2102,7 +2109,12 @@ export type UserLookupAdminPanelMutationVariables = Exact<{ }>; -export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } }; +export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'WorkspaceFeatureFlag', key: string, value: boolean }> }> } }; + +export type GetFeatureFlagManagementCapabilityQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetFeatureFlagManagementCapabilityQuery = { __typename?: 'Query', getFeatureFlagManagementCapability: boolean }; export type CreateOidcIdentityProviderMutationVariables = Exact<{ input: SetupOidcSsoInput; @@ -3668,6 +3680,38 @@ export function useUserLookupAdminPanelMutation(baseOptions?: Apollo.MutationHoo export type UserLookupAdminPanelMutationHookResult = ReturnType; export type UserLookupAdminPanelMutationResult = Apollo.MutationResult; export type UserLookupAdminPanelMutationOptions = Apollo.BaseMutationOptions; +export const GetFeatureFlagManagementCapabilityDocument = gql` + query GetFeatureFlagManagementCapability { + getFeatureFlagManagementCapability +} + `; + +/** + * __useGetFeatureFlagManagementCapabilityQuery__ + * + * To run a query within a React component, call `useGetFeatureFlagManagementCapabilityQuery` and pass it any options that fit your needs. + * When your component renders, `useGetFeatureFlagManagementCapabilityQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetFeatureFlagManagementCapabilityQuery({ + * variables: { + * }, + * }); + */ +export function useGetFeatureFlagManagementCapabilityQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetFeatureFlagManagementCapabilityDocument, options); + } +export function useGetFeatureFlagManagementCapabilityLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetFeatureFlagManagementCapabilityDocument, options); + } +export type GetFeatureFlagManagementCapabilityQueryHookResult = ReturnType; +export type GetFeatureFlagManagementCapabilityLazyQueryHookResult = ReturnType; +export type GetFeatureFlagManagementCapabilityQueryResult = Apollo.QueryResult; export const CreateOidcIdentityProviderDocument = gql` mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) { createOIDCIdentityProvider(input: $input) { diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index 7dcdb1cf18b8..c6830aeebc98 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -1,5 +1,7 @@ import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs'; +import { useFeatureFlagManagementCapability } from '@/settings/admin-panel/hooks/useFeatureFlagManagementCapability'; import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement'; +import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; import { TextInput } from '@/ui/input/components/TextInput'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -24,7 +26,6 @@ import { Toggle, } from 'twenty-ui'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; const StyledLinkContainer = styled.div` margin-right: ${({ theme }) => theme.spacing(2)}; @@ -47,7 +48,7 @@ const StyledUserInfo = styled.div` `; const StyledTable = styled(Table)` - margin-top: ${({ theme }) => theme.spacing(0.5)}; + margin-top: ${({ theme }) => theme.spacing(3)}; `; const StyledTabListContainer = styled.div` @@ -87,6 +88,13 @@ export const SettingsAdminContent = () => { error, } = useFeatureFlagsManagement(); + const { canManageFeatureFlags, isLoading: isLoadingCapability } = + useFeatureFlagManagementCapability(); + + if (isLoadingCapability) { + return null; // Or a loading component + } + const handleSearch = async () => { setActiveTabId(''); @@ -151,37 +159,39 @@ export const SettingsAdminContent = () => { /> )} - - - Feature Flag - Status - - - {activeWorkspace.featureFlags.map((flag) => ( + {canManageFeatureFlags && ( + - {flag.key} - - - handleFeatureFlagUpdate( - activeWorkspace.id, - flag.key, - newValue, - ) - } - /> - + Feature Flag + Status - ))} - + + {activeWorkspace.featureFlags.map((flag) => ( + + {flag.key} + + + handleFeatureFlagUpdate( + activeWorkspace.id, + flag.key, + newValue, + ) + } + /> + + + ))} + + )} ); }; @@ -190,8 +200,16 @@ export const SettingsAdminContent = () => { <>
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getFeatureFlagManagementCapability.ts b/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getFeatureFlagManagementCapability.ts new file mode 100644 index 000000000000..ab2a6bcbc218 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getFeatureFlagManagementCapability.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const GET_FEATURE_FLAG_MANAGEMENT_CAPABILITY = gql` + query GetFeatureFlagManagementCapability { + getFeatureFlagManagementCapability + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagManagementCapability.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagManagementCapability.ts new file mode 100644 index 000000000000..ce9377507f91 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagManagementCapability.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@apollo/client'; +import { GET_FEATURE_FLAG_MANAGEMENT_CAPABILITY } from '../graphql/queries/getFeatureFlagManagementCapability'; + +export const useFeatureFlagManagementCapability = () => { + const { data, loading } = useQuery(GET_FEATURE_FLAG_MANAGEMENT_CAPABILITY); + + return { + canManageFeatureFlags: data?.getFeatureFlagManagementCapability ?? false, + isLoading: loading, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts index ea18ea80b801..2dcd10597d57 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -1,16 +1,16 @@ import { UseFilters, UseGuards } from '@nestjs/common'; -import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input'; +import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output'; import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input'; import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity'; import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; +import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard'; -import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output'; @Resolver() @UseFilters(AuthGraphqlApiExceptionFilter) @@ -46,4 +46,10 @@ export class AdminPanelResolver { return true; } + + @UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard) + @Query(() => Boolean) + async getFeatureFlagManagementCapability(): Promise { + return await this.adminService.getFeatureFlagManagementCapability(); + } } diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index a50708e72e2a..2ead85b69f84 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -8,13 +8,14 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; +import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.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 { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; -import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; @Injectable() export class AdminPanelService { @@ -26,8 +27,16 @@ export class AdminPanelService { private readonly workspaceRepository: Repository, @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, + private readonly environmentService: EnvironmentService, ) {} + private async canManageFeatureFlags(): Promise { + return ( + this.environmentService.get('IS_BILLING_ENABLED') || + this.environmentService.get('DEBUG_MODE') + ); + } + async impersonate(userId: string, workspaceId: string) { const user = await this.userRepository.findOne({ where: { @@ -69,8 +78,13 @@ export class AdminPanelService { }; } + async getFeatureFlagManagementCapability(): Promise { + return await this.canManageFeatureFlags(); + } + async userLookup(userIdentifier: string): Promise { const isEmail = userIdentifier.includes('@'); + const canManageFlags = await this.canManageFeatureFlags(); const targetUser = await this.userRepository.findOne({ where: isEmail ? { email: userIdentifier } : { id: userIdentifier }, @@ -79,7 +93,7 @@ export class AdminPanelService { 'workspaces.workspace', 'workspaces.workspace.workspaceUsers', 'workspaces.workspace.workspaceUsers.user', - 'workspaces.workspace.featureFlags', + ...(canManageFlags ? ['workspaces.workspace.featureFlags'] : []), ], }); @@ -109,13 +123,15 @@ export class AdminPanelService { firstName: workspaceUser.user.firstName, lastName: workspaceUser.user.lastName, })), - featureFlags: allFeatureFlagKeys.map((key) => ({ - key, - value: - userWorkspace.workspace.featureFlags?.find( - (flag) => flag.key === key, - )?.value ?? false, - })) as FeatureFlagEntity[], + featureFlags: canManageFlags + ? allFeatureFlagKeys.map((key) => ({ + key, + value: + userWorkspace.workspace.featureFlags?.find( + (flag) => flag.key === key, + )?.value ?? false, + })) + : [], })), }; } diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts index 461ff566c4f8..26563e8d2e73 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts @@ -1,7 +1,5 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; - @ObjectType() class UserInfo { @Field(() => String) @@ -17,6 +15,15 @@ class UserInfo { lastName?: string; } +@ObjectType() +class WorkspaceFeatureFlag { + @Field(() => String) + key: string; + + @Field(() => Boolean) + value: boolean; +} + @ObjectType() class WorkspaceInfo { @Field(() => String) @@ -37,8 +44,8 @@ class WorkspaceInfo { @Field(() => [UserInfo]) users: UserInfo[]; - @Field(() => [FeatureFlagEntity]) - featureFlags: FeatureFlagEntity[]; + @Field(() => [WorkspaceFeatureFlag]) + featureFlags: WorkspaceFeatureFlag[]; } @ObjectType() From 7d4c99c28d7b3472bfee6d435b6862cddf00dc5d Mon Sep 17 00:00:00 2001 From: ehconitin Date: Thu, 9 Jan 2025 16:55:52 +0530 Subject: [PATCH 4/8] skeletonloader --- .../settings/admin-panel/components/SettingsAdminContent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index c6830aeebc98..a3f554570e8a 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -2,6 +2,7 @@ import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/cons import { useFeatureFlagManagementCapability } from '@/settings/admin-panel/hooks/useFeatureFlagManagementCapability'; import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement'; import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; +import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { TextInput } from '@/ui/input/components/TextInput'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -92,7 +93,7 @@ export const SettingsAdminContent = () => { useFeatureFlagManagementCapability(); if (isLoadingCapability) { - return null; // Or a loading component + return ; } const handleSearch = async () => { From fab2a1bb1cd3d8a19bb03f23d8bd705c75d595d8 Mon Sep 17 00:00:00 2001 From: ehconitin Date: Thu, 9 Jan 2025 17:52:38 +0530 Subject: [PATCH 5/8] review --- .../twenty-front/src/generated/graphql.tsx | 43 ++----------------- .../components/ClientConfigProviderEffect.tsx | 7 +++ .../graphql/queries/getClientConfig.ts | 1 + .../states/canManageFeatureFlagsState.ts | 6 +++ .../components/SettingsAdminContent.tsx | 11 ++--- .../getFeatureFlagManagementCapability.ts | 7 --- .../useFeatureFlagManagementCapability.ts | 11 ----- .../src/testing/mock-data/config.ts | 1 + .../admin-panel/admin-panel.resolver.ts | 8 +--- .../admin-panel/admin-panel.service.ts | 16 +++---- .../admin-panel/dtos/user-lookup.entity.ts | 3 ++ .../client-config/client-config.entity.ts | 3 ++ .../client-config/client-config.resolver.ts | 3 ++ 13 files changed, 39 insertions(+), 81 deletions(-) create mode 100644 packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts delete mode 100644 packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getFeatureFlagManagementCapability.ts delete mode 100644 packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagManagementCapability.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 8fd9c2c24cde..14ddb9826e5e 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -171,6 +171,7 @@ export type ClientConfig = { api: ApiConfig; authProviders: AuthProviders; billing: Billing; + canManageFeatureFlags: Scalars['Boolean']; captcha: Captcha; chromeExtensionId?: Maybe; debugMode: Scalars['Boolean']; @@ -923,7 +924,6 @@ export type Query = { findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; getAvailablePackages: Scalars['JSON']; - getFeatureFlagManagementCapability: Scalars['Boolean']; getPostgresCredentials?: Maybe; getProductPrices: ProductPricesEntity; getPublicWorkspaceDataBySubdomain: PublicWorkspaceDataOutput; @@ -1578,6 +1578,7 @@ export type WorkspaceFeatureFlag = { export type WorkspaceInfo = { __typename?: 'WorkspaceInfo'; allowImpersonation: Scalars['Boolean']; + canManageFeatureFlags: Scalars['Boolean']; featureFlags: Array; id: Scalars['String']; logo?: Maybe; @@ -2088,7 +2089,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -2111,11 +2112,6 @@ export type UserLookupAdminPanelMutationVariables = Exact<{ export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'WorkspaceFeatureFlag', key: string, value: boolean }> }> } }; -export type GetFeatureFlagManagementCapabilityQueryVariables = Exact<{ [key: string]: never; }>; - - -export type GetFeatureFlagManagementCapabilityQuery = { __typename?: 'Query', getFeatureFlagManagementCapability: boolean }; - export type CreateOidcIdentityProviderMutationVariables = Exact<{ input: SetupOidcSsoInput; }>; @@ -3526,6 +3522,7 @@ export const GetClientConfigDocument = gql` mutationMaximumAffectedRecords } chromeExtensionId + canManageFeatureFlags } } `; @@ -3680,38 +3677,6 @@ export function useUserLookupAdminPanelMutation(baseOptions?: Apollo.MutationHoo export type UserLookupAdminPanelMutationHookResult = ReturnType; export type UserLookupAdminPanelMutationResult = Apollo.MutationResult; export type UserLookupAdminPanelMutationOptions = Apollo.BaseMutationOptions; -export const GetFeatureFlagManagementCapabilityDocument = gql` - query GetFeatureFlagManagementCapability { - getFeatureFlagManagementCapability -} - `; - -/** - * __useGetFeatureFlagManagementCapabilityQuery__ - * - * To run a query within a React component, call `useGetFeatureFlagManagementCapabilityQuery` and pass it any options that fit your needs. - * When your component renders, `useGetFeatureFlagManagementCapabilityQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useGetFeatureFlagManagementCapabilityQuery({ - * variables: { - * }, - * }); - */ -export function useGetFeatureFlagManagementCapabilityQuery(baseOptions?: Apollo.QueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(GetFeatureFlagManagementCapabilityDocument, options); - } -export function useGetFeatureFlagManagementCapabilityLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(GetFeatureFlagManagementCapabilityDocument, options); - } -export type GetFeatureFlagManagementCapabilityQueryHookResult = ReturnType; -export type GetFeatureFlagManagementCapabilityLazyQueryHookResult = ReturnType; -export type GetFeatureFlagManagementCapabilityQueryResult = Apollo.QueryResult; export const CreateOidcIdentityProviderDocument = gql` mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) { createOIDCIdentityProvider(input: $input) { diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index ebbfa965ead7..224812287dd3 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -1,6 +1,7 @@ import { apiConfigState } from '@/client-config/states/apiConfigState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; +import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState'; @@ -45,6 +46,10 @@ export const ClientConfigProviderEffect = () => { const setApiConfig = useSetRecoilState(apiConfigState); + const setCanManageFeatureFlags = useSetRecoilState( + canManageFeatureFlagsState, + ); + const { data, loading, error } = useGetClientConfigQuery({ skip: clientConfigApiStatus.isLoaded, }); @@ -107,6 +112,7 @@ export const ClientConfigProviderEffect = () => { defaultSubdomain: data?.clientConfig?.defaultSubdomain, frontDomain: data?.clientConfig?.frontDomain, }); + setCanManageFeatureFlags(data?.clientConfig?.canManageFeatureFlags); }, [ data, setIsDebugMode, @@ -125,6 +131,7 @@ export const ClientConfigProviderEffect = () => { setDomainConfiguration, setIsSSOEnabledState, setAuthProviders, + setCanManageFeatureFlags, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index 88a696368988..57aeb22389c4 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -44,6 +44,7 @@ export const GET_CLIENT_CONFIG = gql` mutationMaximumAffectedRecords } chromeExtensionId + canManageFeatureFlags } } `; diff --git a/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts b/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts new file mode 100644 index 000000000000..1d0222a6ea9d --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/canManageFeatureFlagsState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const canManageFeatureFlagsState = createState({ + key: 'canManageFeatureFlagsState', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index a3f554570e8a..cfaaeec817e3 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -1,8 +1,7 @@ +import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs'; -import { useFeatureFlagManagementCapability } from '@/settings/admin-panel/hooks/useFeatureFlagManagementCapability'; import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement'; import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; -import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { TextInput } from '@/ui/input/components/TextInput'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -14,6 +13,7 @@ import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/consta import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { getImageAbsoluteURI } from 'twenty-shared'; import { Button, @@ -89,12 +89,7 @@ export const SettingsAdminContent = () => { error, } = useFeatureFlagsManagement(); - const { canManageFeatureFlags, isLoading: isLoadingCapability } = - useFeatureFlagManagementCapability(); - - if (isLoadingCapability) { - return ; - } + const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState); const handleSearch = async () => { setActiveTabId(''); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getFeatureFlagManagementCapability.ts b/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getFeatureFlagManagementCapability.ts deleted file mode 100644 index ab2a6bcbc218..000000000000 --- a/packages/twenty-front/src/modules/settings/admin-panel/graphql/queries/getFeatureFlagManagementCapability.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from '@apollo/client'; - -export const GET_FEATURE_FLAG_MANAGEMENT_CAPABILITY = gql` - query GetFeatureFlagManagementCapability { - getFeatureFlagManagementCapability - } -`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagManagementCapability.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagManagementCapability.ts deleted file mode 100644 index ce9377507f91..000000000000 --- a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagManagementCapability.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useQuery } from '@apollo/client'; -import { GET_FEATURE_FLAG_MANAGEMENT_CAPABILITY } from '../graphql/queries/getFeatureFlagManagementCapability'; - -export const useFeatureFlagManagementCapability = () => { - const { data, loading } = useQuery(GET_FEATURE_FLAG_MANAGEMENT_CAPABILITY); - - return { - canManageFeatureFlags: data?.getFeatureFlagManagementCapability ?? false, - isLoading: loading, - }; -}; diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index a80303d9a5e6..97a62869a11c 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -40,4 +40,5 @@ export const mockedClientConfig: ClientConfig = { __typename: 'Captcha', }, api: { mutationMaximumAffectedRecords: 100 }, + canManageFeatureFlags: true, }; diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts index 2dcd10597d57..0e91c3c57d7c 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -1,5 +1,5 @@ import { UseFilters, UseGuards } from '@nestjs/common'; -import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input'; @@ -46,10 +46,4 @@ export class AdminPanelResolver { return true; } - - @UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard) - @Query(() => Boolean) - async getFeatureFlagManagementCapability(): Promise { - return await this.adminService.getFeatureFlagManagementCapability(); - } } diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index 2ead85b69f84..c0679c75a6e8 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -32,8 +32,8 @@ export class AdminPanelService { private async canManageFeatureFlags(): Promise { return ( - this.environmentService.get('IS_BILLING_ENABLED') || - this.environmentService.get('DEBUG_MODE') + this.environmentService.get('DEBUG_MODE') || + this.environmentService.get('IS_BILLING_ENABLED') ); } @@ -78,13 +78,8 @@ export class AdminPanelService { }; } - async getFeatureFlagManagementCapability(): Promise { - return await this.canManageFeatureFlags(); - } - async userLookup(userIdentifier: string): Promise { const isEmail = userIdentifier.includes('@'); - const canManageFlags = await this.canManageFeatureFlags(); const targetUser = await this.userRepository.findOne({ where: isEmail ? { email: userIdentifier } : { id: userIdentifier }, @@ -93,7 +88,7 @@ export class AdminPanelService { 'workspaces.workspace', 'workspaces.workspace.workspaceUsers', 'workspaces.workspace.workspaceUsers.user', - ...(canManageFlags ? ['workspaces.workspace.featureFlags'] : []), + 'workspaces.workspace.featureFlags', ], }); @@ -104,6 +99,8 @@ export class AdminPanelService { const allFeatureFlagKeys = Object.values(FeatureFlagKey); + const canManageFeatureFlags = await this.canManageFeatureFlags(); + return { user: { id: targetUser.id, @@ -123,7 +120,8 @@ export class AdminPanelService { firstName: workspaceUser.user.firstName, lastName: workspaceUser.user.lastName, })), - featureFlags: canManageFlags + canManageFeatureFlags, + featureFlags: canManageFeatureFlags ? allFeatureFlagKeys.map((key) => ({ key, value: diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts index 26563e8d2e73..926d893604aa 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts @@ -44,6 +44,9 @@ class WorkspaceInfo { @Field(() => [UserInfo]) users: UserInfo[]; + @Field(() => Boolean) + canManageFeatureFlags: boolean; + @Field(() => [WorkspaceFeatureFlag]) featureFlags: WorkspaceFeatureFlag[]; } diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index c94b2418e050..6e3842f5c820 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -94,4 +94,7 @@ export class ClientConfig { @Field(() => ApiConfig) api: ApiConfig; + + @Field(() => Boolean) + canManageFeatureFlags: boolean; } diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index cbd1fb7d0181..bc64032070aa 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -59,6 +59,9 @@ export class ClientConfigResolver { ), }, analyticsEnabled: this.environmentService.get('ANALYTICS_ENABLED'), + canManageFeatureFlags: + this.environmentService.get('DEBUG_MODE') || + this.environmentService.get('IS_BILLING_ENABLED'), }; return Promise.resolve(clientConfig); From 12899546c28ea0a8b7a3ff71cf6cb2f41b2810dc Mon Sep 17 00:00:00 2001 From: ehconitin Date: Thu, 9 Jan 2025 18:00:37 +0530 Subject: [PATCH 6/8] review --- .../twenty-front/src/generated/graphql.tsx | 11 ++------ .../admin-panel/admin-panel.service.ts | 28 +++++-------------- .../admin-panel/dtos/user-lookup.entity.ts | 18 +++--------- 3 files changed, 13 insertions(+), 44 deletions(-) diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 14ddb9826e5e..953c6b9e6b3f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1569,17 +1569,10 @@ export type WorkspaceEdge = { node: Workspace; }; -export type WorkspaceFeatureFlag = { - __typename?: 'WorkspaceFeatureFlag'; - key: Scalars['String']; - value: Scalars['Boolean']; -}; - export type WorkspaceInfo = { __typename?: 'WorkspaceInfo'; allowImpersonation: Scalars['Boolean']; - canManageFeatureFlags: Scalars['Boolean']; - featureFlags: Array; + featureFlags: Array; id: Scalars['String']; logo?: Maybe; name: Scalars['String']; @@ -2110,7 +2103,7 @@ export type UserLookupAdminPanelMutationVariables = Exact<{ }>; -export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'WorkspaceFeatureFlag', key: string, value: boolean }> }> } }; +export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } }; export type CreateOidcIdentityProviderMutationVariables = Exact<{ input: SetupOidcSsoInput; diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index c0679c75a6e8..1a4ab62f9370 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -9,7 +9,6 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.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 { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; @@ -27,16 +26,8 @@ export class AdminPanelService { private readonly workspaceRepository: Repository, @InjectRepository(FeatureFlagEntity, 'core') private readonly featureFlagRepository: Repository, - private readonly environmentService: EnvironmentService, ) {} - private async canManageFeatureFlags(): Promise { - return ( - this.environmentService.get('DEBUG_MODE') || - this.environmentService.get('IS_BILLING_ENABLED') - ); - } - async impersonate(userId: string, workspaceId: string) { const user = await this.userRepository.findOne({ where: { @@ -99,8 +90,6 @@ export class AdminPanelService { const allFeatureFlagKeys = Object.values(FeatureFlagKey); - const canManageFeatureFlags = await this.canManageFeatureFlags(); - return { user: { id: targetUser.id, @@ -120,16 +109,13 @@ export class AdminPanelService { firstName: workspaceUser.user.firstName, lastName: workspaceUser.user.lastName, })), - canManageFeatureFlags, - featureFlags: canManageFeatureFlags - ? allFeatureFlagKeys.map((key) => ({ - key, - value: - userWorkspace.workspace.featureFlags?.find( - (flag) => flag.key === key, - )?.value ?? false, - })) - : [], + featureFlags: allFeatureFlagKeys.map((key) => ({ + key, + value: + userWorkspace.workspace.featureFlags?.find( + (flag) => flag.key === key, + )?.value ?? false, + })) as FeatureFlagEntity[], })), }; } diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts index 926d893604aa..461ff566c4f8 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts @@ -1,5 +1,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; + @ObjectType() class UserInfo { @Field(() => String) @@ -15,15 +17,6 @@ class UserInfo { lastName?: string; } -@ObjectType() -class WorkspaceFeatureFlag { - @Field(() => String) - key: string; - - @Field(() => Boolean) - value: boolean; -} - @ObjectType() class WorkspaceInfo { @Field(() => String) @@ -44,11 +37,8 @@ class WorkspaceInfo { @Field(() => [UserInfo]) users: UserInfo[]; - @Field(() => Boolean) - canManageFeatureFlags: boolean; - - @Field(() => [WorkspaceFeatureFlag]) - featureFlags: WorkspaceFeatureFlag[]; + @Field(() => [FeatureFlagEntity]) + featureFlags: FeatureFlagEntity[]; } @ObjectType() From e4f1e632ecc074700e405f67a38f0313fde4c1b3 Mon Sep 17 00:00:00 2001 From: ehconitin Date: Thu, 9 Jan 2025 18:05:35 +0530 Subject: [PATCH 7/8] grammar --- .../settings/admin-panel/components/SettingsAdminContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx index cfaaeec817e3..484932e73bd4 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx @@ -203,7 +203,7 @@ export const SettingsAdminContent = () => { } description={ canManageFeatureFlags - ? 'Look up users and manage their workspace feature flags or impersonate it.' + ? 'Look up users and manage their workspace feature flags or impersonate them.' : 'Look up users to impersonate them.' } /> From 7ff8d65fc0ad92bc0829c027d2b11d371b80a68a Mon Sep 17 00:00:00 2001 From: ehconitin Date: Fri, 10 Jan 2025 14:22:43 +0530 Subject: [PATCH 8/8] feature flag pascal case fix --- .../core-modules/admin-panel/admin-panel.service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index 1a4ab62f9370..9236110dd155 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -125,6 +125,10 @@ export class AdminPanelService { featureFlag: FeatureFlagKey, value: boolean, ) { + const featureFlagValue = Object.entries(FeatureFlagKey).find( + ([key]) => key === featureFlag, + )?.[1] as FeatureFlagKey; + const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId }, relations: ['featureFlags'], @@ -136,14 +140,14 @@ export class AdminPanelService { ); const existingFlag = workspace.featureFlags?.find( - (flag) => flag.key === featureFlag, + (flag) => flag.key === featureFlagValue, ); if (existingFlag) { await this.featureFlagRepository.update(existingFlag.id, { value }); } else { await this.featureFlagRepository.save({ - key: featureFlag, + key: featureFlagValue, value, workspaceId: workspace.id, });