Environment variables in admin panel (read only) - front (#10011)
Frontend for https://github.com/twentyhq/core-team-issues/issues/293 POC - https://github.com/twentyhq/twenty/pull/9903 --------- Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -482,12 +482,10 @@ export enum EnvironmentVariablesGroup {
|
||||
Database = 'Database',
|
||||
Email = 'Email',
|
||||
Frontend = 'Frontend',
|
||||
LLM = 'LLM',
|
||||
Logging = 'Logging',
|
||||
Other = 'Other',
|
||||
QueueConfig = 'QueueConfig',
|
||||
Security = 'Security',
|
||||
ServerConfig = 'ServerConfig',
|
||||
Serverless = 'Serverless',
|
||||
Storage = 'Storage',
|
||||
Support = 'Support',
|
||||
Workspace = 'Workspace'
|
||||
@ -495,7 +493,9 @@ export enum EnvironmentVariablesGroup {
|
||||
|
||||
export type EnvironmentVariablesGroupData = {
|
||||
__typename?: 'EnvironmentVariablesGroupData';
|
||||
groupName: EnvironmentVariablesGroup;
|
||||
description: Scalars['String']['output'];
|
||||
isHiddenOnLoad: Scalars['Boolean']['output'];
|
||||
name: EnvironmentVariablesGroup;
|
||||
subgroups: Array<EnvironmentVariablesSubgroupData>;
|
||||
variables: Array<EnvironmentVariable>;
|
||||
};
|
||||
@ -506,26 +506,28 @@ export type EnvironmentVariablesOutput = {
|
||||
};
|
||||
|
||||
export enum EnvironmentVariablesSubGroup {
|
||||
BillingConfig = 'BillingConfig',
|
||||
CaptchaConfig = 'CaptchaConfig',
|
||||
CloudflareConfig = 'CloudflareConfig',
|
||||
EmailSettings = 'EmailSettings',
|
||||
FrontSupportConfig = 'FrontSupportConfig',
|
||||
ExceptionHandler = 'ExceptionHandler',
|
||||
GoogleAuth = 'GoogleAuth',
|
||||
LambdaConfig = 'LambdaConfig',
|
||||
LLM = 'LLM',
|
||||
MicrosoftAuth = 'MicrosoftAuth',
|
||||
PasswordAuth = 'PasswordAuth',
|
||||
RateLimiting = 'RateLimiting',
|
||||
S3Config = 'S3Config',
|
||||
SSL = 'SSL',
|
||||
SentryConfig = 'SentryConfig',
|
||||
SmtpConfig = 'SmtpConfig',
|
||||
StripeConfig = 'StripeConfig',
|
||||
ServerlessConfig = 'ServerlessConfig',
|
||||
StorageConfig = 'StorageConfig',
|
||||
SupportChatConfig = 'SupportChatConfig',
|
||||
TinybirdConfig = 'TinybirdConfig',
|
||||
Tokens = 'Tokens'
|
||||
TokensDuration = 'TokensDuration'
|
||||
}
|
||||
|
||||
export type EnvironmentVariablesSubgroupData = {
|
||||
__typename?: 'EnvironmentVariablesSubgroupData';
|
||||
subgroupName: EnvironmentVariablesSubGroup;
|
||||
description: Scalars['String']['output'];
|
||||
name: EnvironmentVariablesSubGroup;
|
||||
variables: Array<EnvironmentVariable>;
|
||||
};
|
||||
|
||||
@ -895,6 +897,7 @@ export type Mutation = {
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
updateWorkspace: Workspace;
|
||||
updateWorkspaceFeatureFlag: Scalars['Boolean']['output'];
|
||||
updateWorkspaceMemberRole: WorkspaceMember;
|
||||
uploadFile: Scalars['String']['output'];
|
||||
uploadImage: Scalars['String']['output'];
|
||||
uploadProfilePicture: Scalars['String']['output'];
|
||||
@ -1192,6 +1195,12 @@ export type MutationUpdateWorkspaceFeatureFlagArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkspaceMemberRoleArgs = {
|
||||
roleId?: InputMaybe<Scalars['String']['input']>;
|
||||
workspaceMemberId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationUploadFileArgs = {
|
||||
file: Scalars['Upload']['input'];
|
||||
fileFolder?: InputMaybe<FileFolder>;
|
||||
@ -2166,8 +2175,10 @@ export type WorkspaceMember = {
|
||||
id: Scalars['UUID']['output'];
|
||||
locale?: Maybe<Scalars['String']['output']>;
|
||||
name: FullName;
|
||||
roles?: Maybe<Array<Role>>;
|
||||
timeFormat?: Maybe<WorkspaceMemberTimeFormatEnum>;
|
||||
timeZone?: Maybe<Scalars['String']['output']>;
|
||||
userWorkspaceId?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
/** Date format as Month first, Day first, Year first or system as default */
|
||||
|
||||
@ -414,12 +414,10 @@ export enum EnvironmentVariablesGroup {
|
||||
Database = 'Database',
|
||||
Email = 'Email',
|
||||
Frontend = 'Frontend',
|
||||
LLM = 'LLM',
|
||||
Logging = 'Logging',
|
||||
Other = 'Other',
|
||||
QueueConfig = 'QueueConfig',
|
||||
Security = 'Security',
|
||||
ServerConfig = 'ServerConfig',
|
||||
Serverless = 'Serverless',
|
||||
Storage = 'Storage',
|
||||
Support = 'Support',
|
||||
Workspace = 'Workspace'
|
||||
@ -427,7 +425,9 @@ export enum EnvironmentVariablesGroup {
|
||||
|
||||
export type EnvironmentVariablesGroupData = {
|
||||
__typename?: 'EnvironmentVariablesGroupData';
|
||||
groupName: EnvironmentVariablesGroup;
|
||||
description: Scalars['String'];
|
||||
isHiddenOnLoad: Scalars['Boolean'];
|
||||
name: EnvironmentVariablesGroup;
|
||||
subgroups: Array<EnvironmentVariablesSubgroupData>;
|
||||
variables: Array<EnvironmentVariable>;
|
||||
};
|
||||
@ -438,26 +438,28 @@ export type EnvironmentVariablesOutput = {
|
||||
};
|
||||
|
||||
export enum EnvironmentVariablesSubGroup {
|
||||
BillingConfig = 'BillingConfig',
|
||||
CaptchaConfig = 'CaptchaConfig',
|
||||
CloudflareConfig = 'CloudflareConfig',
|
||||
EmailSettings = 'EmailSettings',
|
||||
FrontSupportConfig = 'FrontSupportConfig',
|
||||
ExceptionHandler = 'ExceptionHandler',
|
||||
GoogleAuth = 'GoogleAuth',
|
||||
LambdaConfig = 'LambdaConfig',
|
||||
LLM = 'LLM',
|
||||
MicrosoftAuth = 'MicrosoftAuth',
|
||||
PasswordAuth = 'PasswordAuth',
|
||||
RateLimiting = 'RateLimiting',
|
||||
S3Config = 'S3Config',
|
||||
SSL = 'SSL',
|
||||
SentryConfig = 'SentryConfig',
|
||||
SmtpConfig = 'SmtpConfig',
|
||||
StripeConfig = 'StripeConfig',
|
||||
ServerlessConfig = 'ServerlessConfig',
|
||||
StorageConfig = 'StorageConfig',
|
||||
SupportChatConfig = 'SupportChatConfig',
|
||||
TinybirdConfig = 'TinybirdConfig',
|
||||
Tokens = 'Tokens'
|
||||
TokensDuration = 'TokensDuration'
|
||||
}
|
||||
|
||||
export type EnvironmentVariablesSubgroupData = {
|
||||
__typename?: 'EnvironmentVariablesSubgroupData';
|
||||
subgroupName: EnvironmentVariablesSubGroup;
|
||||
description: Scalars['String'];
|
||||
name: EnvironmentVariablesSubGroup;
|
||||
variables: Array<EnvironmentVariable>;
|
||||
};
|
||||
|
||||
@ -812,6 +814,7 @@ export type Mutation = {
|
||||
updateWorkflowVersionStep: WorkflowAction;
|
||||
updateWorkspace: Workspace;
|
||||
updateWorkspaceFeatureFlag: Scalars['Boolean'];
|
||||
updateWorkspaceMemberRole: WorkspaceMember;
|
||||
uploadFile: Scalars['String'];
|
||||
uploadImage: Scalars['String'];
|
||||
uploadProfilePicture: Scalars['String'];
|
||||
@ -1059,6 +1062,12 @@ export type MutationUpdateWorkspaceFeatureFlagArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateWorkspaceMemberRoleArgs = {
|
||||
roleId?: InputMaybe<Scalars['String']>;
|
||||
workspaceMemberId: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationUploadFileArgs = {
|
||||
file: Scalars['Upload'];
|
||||
fileFolder?: InputMaybe<FileFolder>;
|
||||
@ -1944,8 +1953,10 @@ export type WorkspaceMember = {
|
||||
id: Scalars['UUID'];
|
||||
locale?: Maybe<Scalars['String']>;
|
||||
name: FullName;
|
||||
roles?: Maybe<Array<Role>>;
|
||||
timeFormat?: Maybe<WorkspaceMemberTimeFormatEnum>;
|
||||
timeZone?: Maybe<Scalars['String']>;
|
||||
userWorkspaceId?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
/** Date format as Month first, Day first, Year first or system as default */
|
||||
@ -2233,7 +2244,7 @@ export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookup
|
||||
export type GetEnvironmentVariablesGroupedQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetEnvironmentVariablesGroupedQuery = { __typename?: 'Query', getEnvironmentVariablesGrouped: { __typename?: 'EnvironmentVariablesOutput', groups: Array<{ __typename?: 'EnvironmentVariablesGroupData', groupName: EnvironmentVariablesGroup, variables: Array<{ __typename?: 'EnvironmentVariable', name: string, description: string, value: string, sensitive: boolean }>, subgroups: Array<{ __typename?: 'EnvironmentVariablesSubgroupData', subgroupName: EnvironmentVariablesSubGroup, variables: Array<{ __typename?: 'EnvironmentVariable', name: string, description: string, value: string, sensitive: boolean }> }> }> } };
|
||||
export type GetEnvironmentVariablesGroupedQuery = { __typename?: 'Query', getEnvironmentVariablesGrouped: { __typename?: 'EnvironmentVariablesOutput', groups: Array<{ __typename?: 'EnvironmentVariablesGroupData', name: EnvironmentVariablesGroup, description: string, isHiddenOnLoad: boolean, variables: Array<{ __typename?: 'EnvironmentVariable', name: string, description: string, value: string, sensitive: boolean }>, subgroups: Array<{ __typename?: 'EnvironmentVariablesSubgroupData', name: EnvironmentVariablesSubGroup, description: string, variables: Array<{ __typename?: 'EnvironmentVariable', name: string, description: string, value: string, sensitive: boolean }> }> }> } };
|
||||
|
||||
export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
|
||||
input: UpdateLabPublicFeatureFlagInput;
|
||||
@ -3859,7 +3870,9 @@ export const GetEnvironmentVariablesGroupedDocument = gql`
|
||||
query GetEnvironmentVariablesGrouped {
|
||||
getEnvironmentVariablesGrouped {
|
||||
groups {
|
||||
groupName
|
||||
name
|
||||
description
|
||||
isHiddenOnLoad
|
||||
variables {
|
||||
name
|
||||
description
|
||||
@ -3867,7 +3880,8 @@ export const GetEnvironmentVariablesGroupedDocument = gql`
|
||||
sensitive
|
||||
}
|
||||
subgroups {
|
||||
subgroupName
|
||||
name
|
||||
description
|
||||
variables {
|
||||
name
|
||||
description
|
||||
|
||||
@ -1,55 +1,9 @@
|
||||
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
|
||||
import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs';
|
||||
import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement';
|
||||
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent';
|
||||
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
|
||||
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
|
||||
import {
|
||||
Button,
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
H2Title,
|
||||
IconSearch,
|
||||
IconUser,
|
||||
Section,
|
||||
Toggle,
|
||||
} from 'twenty-ui';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledErrorSection = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.danger};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledUserInfo = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
import { IconSettings2, IconVariable } from 'twenty-ui';
|
||||
|
||||
const StyledTabListContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -59,210 +13,30 @@ const StyledTabListContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: ${({ theme }) => theme.spacing(4)} 0;
|
||||
`;
|
||||
|
||||
export const SettingsAdminContent = () => {
|
||||
const [userIdentifier, setUserIdentifier] = useState('');
|
||||
const [userId, setUserId] = useState('');
|
||||
|
||||
const {
|
||||
handleImpersonate,
|
||||
isLoading: isImpersonateLoading,
|
||||
error: impersonateError,
|
||||
canImpersonate,
|
||||
} = useImpersonate();
|
||||
|
||||
const { activeTabId, setActiveTabId } = useTabList(
|
||||
SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID,
|
||||
);
|
||||
|
||||
const {
|
||||
userLookupResult,
|
||||
handleUserLookup,
|
||||
handleFeatureFlagUpdate,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFeatureFlagsManagement();
|
||||
|
||||
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
|
||||
|
||||
const handleSearch = async () => {
|
||||
setActiveTabId('');
|
||||
|
||||
const result = await handleUserLookup(userIdentifier);
|
||||
|
||||
if (isDefined(result?.user?.id) && !error) {
|
||||
setUserId(result.user.id.trim());
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(result?.workspaces) &&
|
||||
result.workspaces.length > 0 &&
|
||||
!error
|
||||
) {
|
||||
setActiveTabId(result.workspaces[0].id);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowUserData = userLookupResult && !error;
|
||||
|
||||
const activeWorkspace = userLookupResult?.workspaces.find(
|
||||
(workspace) => workspace.id === activeTabId,
|
||||
);
|
||||
|
||||
const tabs =
|
||||
userLookupResult?.workspaces.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
title: workspace.name,
|
||||
logo:
|
||||
getImageAbsoluteURI({
|
||||
imageUrl: isNonEmptyString(workspace.logo)
|
||||
? workspace.logo
|
||||
: DEFAULT_WORKSPACE_LOGO,
|
||||
baseUrl: REACT_APP_SERVER_BASE_URL,
|
||||
}) ?? '',
|
||||
})) ?? [];
|
||||
|
||||
const renderWorkspaceContent = () => {
|
||||
if (!activeWorkspace) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<H2Title title={activeWorkspace.name} description={'Workspace Name'} />
|
||||
<H2Title
|
||||
title={`${activeWorkspace.totalUsers} ${
|
||||
activeWorkspace.totalUsers > 1 ? 'Users' : 'User'
|
||||
}`}
|
||||
description={'Total Users'}
|
||||
/>
|
||||
{canImpersonate && (
|
||||
<Button
|
||||
Icon={IconUser}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
title={'Impersonate'}
|
||||
onClick={() => handleImpersonate(userId, activeWorkspace.id)}
|
||||
disabled={
|
||||
isImpersonateLoading ||
|
||||
activeWorkspace.allowImpersonation === false
|
||||
}
|
||||
dataTestId="impersonate-button"
|
||||
/>
|
||||
)}
|
||||
|
||||
{canManageFeatureFlags && (
|
||||
<StyledTable>
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 100px"
|
||||
mobileGridAutoColumns="1fr 80px"
|
||||
>
|
||||
<TableHeader>Feature Flag</TableHeader>
|
||||
<TableHeader align="right">Status</TableHeader>
|
||||
</TableRow>
|
||||
|
||||
{activeWorkspace.featureFlags.map((flag) => (
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 100px"
|
||||
mobileGridAutoColumns="1fr 80px"
|
||||
key={flag.key}
|
||||
>
|
||||
<TableCell>{flag.key}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Toggle
|
||||
value={flag.value}
|
||||
onChange={(newValue) =>
|
||||
handleFeatureFlagUpdate(
|
||||
activeWorkspace.id,
|
||||
flag.key,
|
||||
newValue,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</StyledTable>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const tabs = [
|
||||
{
|
||||
id: SETTINGS_ADMIN_TABS.GENERAL,
|
||||
title: 'General',
|
||||
Icon: IconSettings2,
|
||||
},
|
||||
{
|
||||
id: SETTINGS_ADMIN_TABS.ENV_VARIABLES,
|
||||
title: 'Env Variables',
|
||||
Icon: IconVariable,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={
|
||||
canManageFeatureFlags
|
||||
? 'Feature Flags & Impersonation'
|
||||
: 'User Impersonation'
|
||||
}
|
||||
description={
|
||||
canManageFeatureFlags
|
||||
? 'Look up users and manage their workspace feature flags or impersonate them.'
|
||||
: 'Look up users to impersonate them.'
|
||||
}
|
||||
<StyledTabListContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
tabListInstanceId={SETTINGS_ADMIN_TABS_ID}
|
||||
behaveAsLinks={true}
|
||||
/>
|
||||
|
||||
<StyledContainer>
|
||||
<StyledLinkContainer>
|
||||
<TextInput
|
||||
value={userIdentifier}
|
||||
onChange={setUserIdentifier}
|
||||
onInputEnter={handleSearch}
|
||||
placeholder="Enter user ID or email address"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</StyledLinkContainer>
|
||||
<Button
|
||||
Icon={IconSearch}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
title="Search"
|
||||
onClick={handleSearch}
|
||||
disabled={!userIdentifier.trim() || isLoading}
|
||||
/>
|
||||
</StyledContainer>
|
||||
|
||||
{(error || impersonateError) && (
|
||||
<StyledErrorSection>{error ?? impersonateError}</StyledErrorSection>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{shouldShowUserData && (
|
||||
<Section>
|
||||
<StyledUserInfo>
|
||||
<H1Title title="User Info" fontColor={H1TitleFontColor.Primary} />
|
||||
<H2Title
|
||||
title={`${userLookupResult.user.firstName || ''} ${
|
||||
userLookupResult.user.lastName || ''
|
||||
}`.trim()}
|
||||
description="User Name"
|
||||
/>
|
||||
<H2Title
|
||||
title={userLookupResult.user.email}
|
||||
description="User Email"
|
||||
/>
|
||||
<H2Title title={userLookupResult.user.id} description="User ID" />
|
||||
</StyledUserInfo>
|
||||
|
||||
<H1Title title="Workspaces" fontColor={H1TitleFontColor.Primary} />
|
||||
<StyledTabListContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
tabListInstanceId={SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
<StyledContentContainer>
|
||||
{renderWorkspaceContent()}
|
||||
</StyledContentContainer>
|
||||
</Section>
|
||||
)}
|
||||
</StyledTabListContainer>
|
||||
<SettingsAdminTabContent />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,113 @@
|
||||
import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { Button, H1Title, H1TitleFontColor, Section } from 'twenty-ui';
|
||||
import { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql';
|
||||
|
||||
const StyledGroupContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledSubGroupContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledGroupVariablesContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(4)};
|
||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledSubGroupTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSubGroupDescription = styled.div``;
|
||||
|
||||
const StyledGroupDescription = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledShowMoreButton = styled(Button)``;
|
||||
|
||||
export const SettingsAdminEnvVariables = () => {
|
||||
const { data: environmentVariables } = useGetEnvironmentVariablesGroupedQuery(
|
||||
{
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
);
|
||||
|
||||
const [showHiddenGroups, setShowHiddenGroups] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
const toggleGroupVisibility = (groupName: string) => {
|
||||
setShowHiddenGroups((prev) => ({
|
||||
...prev,
|
||||
[groupName]: !prev[groupName],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
{environmentVariables?.getEnvironmentVariablesGrouped.groups.map(
|
||||
(group) => {
|
||||
const isHidden =
|
||||
group.isHiddenOnLoad && !showHiddenGroups[group.name];
|
||||
if (isHidden === true) {
|
||||
return (
|
||||
<StyledShowMoreButton
|
||||
key={group.name}
|
||||
onClick={() => toggleGroupVisibility(group.name)}
|
||||
title={
|
||||
showHiddenGroups[group.name] ? 'Show Less' : 'Show More...'
|
||||
}
|
||||
></StyledShowMoreButton>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StyledGroupContainer key={group.name}>
|
||||
<H1Title
|
||||
title={group.name}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
{group.description !== '' && (
|
||||
<StyledGroupDescription>
|
||||
{group.description}
|
||||
</StyledGroupDescription>
|
||||
)}
|
||||
{group.variables.length > 0 && (
|
||||
<StyledGroupVariablesContainer>
|
||||
<SettingsAdminEnvVariablesTable variables={group.variables} />
|
||||
</StyledGroupVariablesContainer>
|
||||
)}
|
||||
{group.subgroups.map((subgroup) => (
|
||||
<StyledSubGroupContainer key={subgroup.name}>
|
||||
<StyledSubGroupTitle>{subgroup.name}</StyledSubGroupTitle>
|
||||
{subgroup.description !== '' && (
|
||||
<StyledSubGroupDescription>
|
||||
{subgroup.description}
|
||||
</StyledSubGroupDescription>
|
||||
)}
|
||||
<SettingsAdminEnvVariablesTable
|
||||
variables={subgroup.variables}
|
||||
/>
|
||||
</StyledSubGroupContainer>
|
||||
))}
|
||||
</StyledGroupContainer>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,130 @@
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
AnimatedExpandableContainer,
|
||||
IconChevronRight,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
LightIconButton,
|
||||
} from 'twenty-ui';
|
||||
type SettingsAdminEnvVariablesRowProps = {
|
||||
variable: {
|
||||
name: string;
|
||||
description: string;
|
||||
value: string;
|
||||
sensitive: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const StyledTruncatedCell = styled(TableCell)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const StyledExpandedDetails = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
margin: ${({ theme }) => theme.spacing(2)} 0;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: fit-content;
|
||||
min-height: min-content;
|
||||
`;
|
||||
|
||||
const StyledDetailLabel = styled.div`
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledEllipsisLabel = styled.div`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledExpandedLabel = styled.div`
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
`;
|
||||
|
||||
const StyledTransitionedIconChevronRight = styled(IconChevronRight)`
|
||||
cursor: pointer;
|
||||
transform: ${({ $isExpanded }: { $isExpanded: boolean }) =>
|
||||
$isExpanded ? 'rotate(90deg)' : 'rotate(0deg)'};
|
||||
transition: ${({ theme }) =>
|
||||
`transform ${theme.animation.duration.normal}s ease`};
|
||||
`;
|
||||
|
||||
export const SettingsAdminEnvVariablesRow = ({
|
||||
variable,
|
||||
}: SettingsAdminEnvVariablesRowProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showSensitiveValue, setShowSensitiveValue] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const displayValue =
|
||||
variable.value === ''
|
||||
? 'null'
|
||||
: variable.sensitive && !showSensitiveValue
|
||||
? '••••••'
|
||||
: variable.value;
|
||||
|
||||
const handleToggleVisibility = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setShowSensitiveValue(!showSensitiveValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
gridAutoColumns="4fr 3fr 2fr 1fr 1fr"
|
||||
>
|
||||
<StyledTruncatedCell color="primary">
|
||||
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
|
||||
</StyledTruncatedCell>
|
||||
<StyledTruncatedCell>
|
||||
<StyledEllipsisLabel>{variable.description}</StyledEllipsisLabel>
|
||||
</StyledTruncatedCell>
|
||||
<StyledTruncatedCell>
|
||||
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
|
||||
</StyledTruncatedCell>
|
||||
<TableCell align="right">
|
||||
{variable.sensitive && variable.value !== '' && (
|
||||
<LightIconButton
|
||||
Icon={showSensitiveValue ? IconEyeOff : IconEye}
|
||||
size="small"
|
||||
accent="secondary"
|
||||
onClick={handleToggleVisibility}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<StyledTransitionedIconChevronRight
|
||||
$isExpanded={isExpanded}
|
||||
size={theme.icon.size.sm}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
|
||||
<StyledExpandedDetails>
|
||||
<StyledDetailLabel>Name:</StyledDetailLabel>
|
||||
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
|
||||
<StyledDetailLabel>Description:</StyledDetailLabel>
|
||||
<StyledExpandedLabel>{variable.description}</StyledExpandedLabel>
|
||||
<StyledDetailLabel>Value:</StyledDetailLabel>
|
||||
<StyledExpandedLabel>{displayValue}</StyledExpandedLabel>
|
||||
</StyledExpandedDetails>
|
||||
</AnimatedExpandableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { SettingsAdminEnvVariablesRow } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesRow';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
type SettingsAdminEnvVariablesTableProps = {
|
||||
variables: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
value: string;
|
||||
sensitive: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const SettingsAdminEnvVariablesTable = ({
|
||||
variables,
|
||||
}: SettingsAdminEnvVariablesTableProps) => (
|
||||
<StyledTable>
|
||||
<TableRow gridAutoColumns="4fr 3fr 2fr 1fr 1fr">
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader>Description</TableHeader>
|
||||
<TableHeader>Value</TableHeader>
|
||||
<TableHeader align="right"></TableHeader>
|
||||
<TableHeader align="right"></TableHeader>
|
||||
</TableRow>
|
||||
{variables.map((variable) => (
|
||||
<SettingsAdminEnvVariablesRow key={variable.name} variable={variable} />
|
||||
))}
|
||||
</StyledTable>
|
||||
);
|
||||
@ -0,0 +1,192 @@
|
||||
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
|
||||
import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent';
|
||||
import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
|
||||
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
|
||||
import { useUserLookup } from '@/settings/admin-panel/hooks/useUserLookup';
|
||||
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
|
||||
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
|
||||
import {
|
||||
Button,
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
H2Title,
|
||||
IconSearch,
|
||||
Section,
|
||||
} from 'twenty-ui';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledErrorSection = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.danger};
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledUserInfo = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
const StyledTabListContainer = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: ${({ theme }) => theme.spacing(4)} 0;
|
||||
`;
|
||||
|
||||
export const SettingsAdminGeneral = () => {
|
||||
const [userIdentifier, setUserIdentifier] = useState('');
|
||||
const [userId, setUserId] = useState('');
|
||||
|
||||
const { error: impersonateError } = useImpersonate();
|
||||
|
||||
const { activeTabId, setActiveTabId } = useTabList(
|
||||
SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID,
|
||||
);
|
||||
const userLookupResult = useRecoilValue(userLookupResultState);
|
||||
const adminPanelError = useRecoilValue(adminPanelErrorState);
|
||||
|
||||
const { handleUserLookup, isLoading } = useUserLookup();
|
||||
|
||||
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
|
||||
|
||||
const handleSearch = async () => {
|
||||
setActiveTabId('');
|
||||
|
||||
const result = await handleUserLookup(userIdentifier);
|
||||
|
||||
if (isDefined(result?.user?.id) && !adminPanelError) {
|
||||
setUserId(result.user.id.trim());
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(result?.workspaces) &&
|
||||
result.workspaces.length > 0 &&
|
||||
!adminPanelError
|
||||
) {
|
||||
setActiveTabId(result.workspaces[0].id);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowUserData = userLookupResult && !adminPanelError;
|
||||
|
||||
const activeWorkspace = userLookupResult?.workspaces.find(
|
||||
(workspace) => workspace.id === activeTabId,
|
||||
);
|
||||
|
||||
const tabs =
|
||||
userLookupResult?.workspaces.map((workspace) => ({
|
||||
id: workspace.id,
|
||||
title: workspace.name,
|
||||
logo:
|
||||
getImageAbsoluteURI({
|
||||
imageUrl: isNonEmptyString(workspace.logo)
|
||||
? workspace.logo
|
||||
: DEFAULT_WORKSPACE_LOGO,
|
||||
baseUrl: REACT_APP_SERVER_BASE_URL,
|
||||
}) ?? '',
|
||||
})) ?? [];
|
||||
|
||||
const userFullName = `${userLookupResult?.user.firstName || ''} ${
|
||||
userLookupResult?.user.lastName || ''
|
||||
}`.trim();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={
|
||||
canManageFeatureFlags
|
||||
? 'Feature Flags & Impersonation'
|
||||
: 'User Impersonation'
|
||||
}
|
||||
description={
|
||||
canManageFeatureFlags
|
||||
? 'Look up users and manage their workspace feature flags or impersonate them.'
|
||||
: 'Look up users to impersonate them.'
|
||||
}
|
||||
/>
|
||||
|
||||
<StyledContainer>
|
||||
<StyledLinkContainer>
|
||||
<TextInput
|
||||
value={userIdentifier}
|
||||
onChange={setUserIdentifier}
|
||||
onInputEnter={handleSearch}
|
||||
placeholder="Enter user ID or email address"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</StyledLinkContainer>
|
||||
<Button
|
||||
Icon={IconSearch}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
title="Search"
|
||||
onClick={handleSearch}
|
||||
disabled={!userIdentifier.trim() || isLoading}
|
||||
/>
|
||||
</StyledContainer>
|
||||
|
||||
{(adminPanelError || impersonateError) && (
|
||||
<StyledErrorSection>
|
||||
{adminPanelError ?? impersonateError}
|
||||
</StyledErrorSection>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{shouldShowUserData && (
|
||||
<Section>
|
||||
<StyledUserInfo>
|
||||
<H1Title title="User Info" fontColor={H1TitleFontColor.Primary} />
|
||||
<H2Title title={userFullName} description="User Name" />
|
||||
<H2Title
|
||||
title={userLookupResult.user.email}
|
||||
description="User Email"
|
||||
/>
|
||||
<H2Title title={userLookupResult.user.id} description="User ID" />
|
||||
</StyledUserInfo>
|
||||
|
||||
<H1Title title="Workspaces" fontColor={H1TitleFontColor.Primary} />
|
||||
<StyledTabListContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
tabListInstanceId={SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
<StyledContentContainer>
|
||||
<SettingsAdminWorkspaceContent
|
||||
activeWorkspace={activeWorkspace}
|
||||
userId={userId}
|
||||
/>
|
||||
</StyledContentContainer>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { SettingsAdminEnvVariables } from '@/settings/admin-panel/components/SettingsAdminEnvVariables';
|
||||
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
|
||||
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
|
||||
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
|
||||
export const SettingsAdminTabContent = () => {
|
||||
const { activeTabId } = useTabList(SETTINGS_ADMIN_TABS_ID);
|
||||
|
||||
switch (activeTabId) {
|
||||
case SETTINGS_ADMIN_TABS.GENERAL:
|
||||
return <SettingsAdminGeneral />;
|
||||
case SETTINGS_ADMIN_TABS.ENV_VARIABLES:
|
||||
return <SettingsAdminEnvVariables />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,97 @@
|
||||
import { Button, H2Title, IconUser, Toggle } from 'twenty-ui';
|
||||
|
||||
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
|
||||
import { useFeatureFlag } from '@/settings/admin-panel/hooks/useFeatureFlag';
|
||||
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
|
||||
import { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
type SettingsAdminWorkspaceContentProps = {
|
||||
activeWorkspace: WorkspaceInfo | undefined;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export const SettingsAdminWorkspaceContent = ({
|
||||
activeWorkspace,
|
||||
userId,
|
||||
}: SettingsAdminWorkspaceContentProps) => {
|
||||
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
|
||||
|
||||
const {
|
||||
handleImpersonate,
|
||||
isLoading: isImpersonateLoading,
|
||||
canImpersonate,
|
||||
} = useImpersonate();
|
||||
|
||||
const { handleFeatureFlagUpdate } = useFeatureFlag();
|
||||
|
||||
if (!activeWorkspace) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<H2Title title={activeWorkspace.name} description={'Workspace Name'} />
|
||||
<H2Title
|
||||
title={`${activeWorkspace.totalUsers} ${
|
||||
activeWorkspace.totalUsers > 1 ? 'Users' : 'User'
|
||||
}`}
|
||||
description={'Total Users'}
|
||||
/>
|
||||
{canImpersonate && (
|
||||
<Button
|
||||
Icon={IconUser}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
title={'Impersonate'}
|
||||
onClick={() => handleImpersonate(userId, activeWorkspace.id)}
|
||||
disabled={
|
||||
isImpersonateLoading || activeWorkspace.allowImpersonation === false
|
||||
}
|
||||
dataTestId="impersonate-button"
|
||||
/>
|
||||
)}
|
||||
|
||||
{canManageFeatureFlags && (
|
||||
<StyledTable>
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 100px"
|
||||
mobileGridAutoColumns="1fr 80px"
|
||||
>
|
||||
<TableHeader>Feature Flag</TableHeader>
|
||||
<TableHeader align="right">Status</TableHeader>
|
||||
</TableRow>
|
||||
|
||||
{activeWorkspace.featureFlags.map((flag) => (
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 100px"
|
||||
mobileGridAutoColumns="1fr 80px"
|
||||
key={flag.key}
|
||||
>
|
||||
<TableCell>{flag.key}</TableCell>
|
||||
<TableCell align="right">
|
||||
<Toggle
|
||||
value={flag.value}
|
||||
onChange={(newValue) =>
|
||||
handleFeatureFlagUpdate(
|
||||
activeWorkspace.id,
|
||||
flag.key,
|
||||
newValue,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</StyledTable>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export const SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID =
|
||||
'settings-admin-feature-flags-tab-id';
|
||||
@ -0,0 +1,4 @@
|
||||
export const SETTINGS_ADMIN_TABS = {
|
||||
GENERAL: 'general',
|
||||
ENV_VARIABLES: 'env-variables',
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const SETTINGS_ADMIN_TABS_ID = 'settings-admin-tabs-id';
|
||||
@ -0,0 +1,2 @@
|
||||
export const SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID =
|
||||
'settings-admin-user-lookup-workspace-tabs-id';
|
||||
@ -4,7 +4,9 @@ export const GET_ENVIRONMENT_VARIABLES_GROUPED = gql`
|
||||
query GetEnvironmentVariablesGrouped {
|
||||
getEnvironmentVariablesGrouped {
|
||||
groups {
|
||||
groupName
|
||||
name
|
||||
description
|
||||
isHiddenOnLoad
|
||||
variables {
|
||||
name
|
||||
description
|
||||
@ -12,7 +14,8 @@ export const GET_ENVIRONMENT_VARIABLES_GROUPED = gql`
|
||||
sensitive
|
||||
}
|
||||
subgroups {
|
||||
subgroupName
|
||||
name
|
||||
description
|
||||
variables {
|
||||
name
|
||||
description
|
||||
|
||||
@ -1,46 +1,21 @@
|
||||
import { UserLookup } from '@/settings/admin-panel/types/UserLookup';
|
||||
import { useState } from 'react';
|
||||
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
|
||||
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
useUpdateWorkspaceFeatureFlagMutation,
|
||||
useUserLookupAdminPanelMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const useFeatureFlagsManagement = () => {
|
||||
const [userLookupResult, setUserLookupResult] = useState<UserLookup | null>(
|
||||
null,
|
||||
export const useFeatureFlag = () => {
|
||||
const [userLookupResult, setUserLookupResult] = useRecoilState(
|
||||
userLookupResultState,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [userLookup] = useUserLookupAdminPanelMutation({
|
||||
onCompleted: (data) => {
|
||||
setIsLoading(false);
|
||||
if (isDefined(data?.userLookupAdminPanel)) {
|
||||
setUserLookupResult(data.userLookupAdminPanel);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsLoading(false);
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
const setError = useSetRecoilState(adminPanelErrorState);
|
||||
|
||||
const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation();
|
||||
|
||||
const handleUserLookup = async (userIdentifier: string) => {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
setUserLookupResult(null);
|
||||
|
||||
const response = await userLookup({
|
||||
variables: { userIdentifier },
|
||||
});
|
||||
|
||||
return response.data?.userLookupAdminPanel;
|
||||
};
|
||||
|
||||
const handleFeatureFlagUpdate = async (
|
||||
workspaceId: string,
|
||||
featureFlag: FeatureFlagKey,
|
||||
@ -83,10 +58,6 @@ export const useFeatureFlagsManagement = () => {
|
||||
};
|
||||
|
||||
return {
|
||||
userLookupResult,
|
||||
handleUserLookup,
|
||||
handleFeatureFlagUpdate,
|
||||
isLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,42 @@
|
||||
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
|
||||
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
|
||||
import { useState } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
|
||||
|
||||
export const useUserLookup = () => {
|
||||
const setUserLookupResult = useSetRecoilState(userLookupResultState);
|
||||
const setError = useSetRecoilState(adminPanelErrorState);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [userLookup] = useUserLookupAdminPanelMutation({
|
||||
onCompleted: (data) => {
|
||||
setIsLoading(false);
|
||||
if (isDefined(data?.userLookupAdminPanel)) {
|
||||
setUserLookupResult(data.userLookupAdminPanel);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsLoading(false);
|
||||
setError(error.message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleUserLookup = async (userIdentifier: string) => {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
setUserLookupResult(null);
|
||||
|
||||
const response = await userLookup({
|
||||
variables: { userIdentifier },
|
||||
});
|
||||
|
||||
return response.data?.userLookupAdminPanel;
|
||||
};
|
||||
|
||||
return {
|
||||
handleUserLookup,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const adminPanelErrorState = atom<string | null>({
|
||||
key: 'adminPanelErrorState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
import { UserLookup } from '@/settings/admin-panel/types/UserLookup';
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const userLookupResultState = atom<UserLookup | null>({
|
||||
key: 'userLookupResultState',
|
||||
default: null,
|
||||
});
|
||||
@ -14,7 +14,6 @@ FRONT_PORT=3001
|
||||
# ———————— Optional ————————
|
||||
# PORT=3000
|
||||
# DEBUG_MODE=true
|
||||
# DEBUG_PORT=9000
|
||||
# ACCESS_TOKEN_EXPIRES_IN=30m
|
||||
# LOGIN_TOKEN_EXPIRES_IN=15m
|
||||
# REFRESH_TOKEN_EXPIRES_IN=90d
|
||||
@ -51,7 +50,6 @@ FRONT_PORT=3001
|
||||
# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
||||
# SENTRY_FRONT_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
||||
# LOG_LEVELS=error,warn
|
||||
# DEMO_WORKSPACE_IDS=REPLACE_ME_WITH_A_RANDOM_UUID
|
||||
# SERVER_URL=http://localhost:3000
|
||||
# WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION=30
|
||||
# WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION=60
|
||||
|
||||
@ -2,12 +2,10 @@ PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/test
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
DEBUG_MODE=true
|
||||
DEBUG_PORT=9000
|
||||
APP_SECRET=replace_me_with_a_random_string
|
||||
SIGN_IN_PREFILLED=true
|
||||
EXCEPTION_HANDLER_DRIVER=console
|
||||
SENTRY_DSN=https://ba869cb8fd72d5faeb6643560939cee0@o4505516959793152.ingest.sentry.io/4506660900306944
|
||||
DEMO_WORKSPACE_IDS=63db4589-590f-42b3-bdf1-85268b3da02f,8de58f3f-7e86-4a0b-998d-b2cbe314ee3a,4d957b72-0b37-4bad-9468-8dc828ee082d,daa0b739-269e-49b6-9be5-5f0215941489,59c15f6a-909a-4495-9cf4-3ce1b0abbb6a,7202cc9d-92da-4b52-a323-d29d38cd3b4e,5f071b0d-646b-411a-94f1-5d9ba9d5c6ac,7bc10973-897b-4767-ab2f-35cdac3b2aec,4b3ba0be-2d29-4b1e-be66-8ac7eb65d000,edfb500d-cc4e-4f22-8e2b-f139a9758a68,eee459c9-9057-4459-ae0d-d51d14c01635,3dd2f505-0075-4217-ba33-fc4244aeaaa9,3d1a9165-3f3f-494e-a99d-f858eae95144,84db6ded-cfce-4aee-9160-6553b05c8143,96fb1540-269b-4d13-af21-2a8268eff8ca,b2463e69-d121-4ea5-80c9-bba82403e93e,5af30c15-867d-49ed-b939-d4856bed8514,b5677aa1-68fa-4818-aaaa-434a07ae2ed4,1ec7fa9a-d6bf-4fa2-a753-9a235d75ee3f,753a6fa2-df27-4c87-8c90-4da78fcb30dd,2138f2f2-bbe9-41df-b483-687a9075f94e,a885cfef-4636-4c3a-9788-1ff6e6b92df5,5458f7fb-9431-47a2-b7a0-32f31d115e23,6c09929f-11c3-4f92-9508-aa0e6b934d1e,57ae0a2c-7a4e-4c7d-8f43-68548e7f1206,cc7f0b85-6868-4c2d-85c5-3ce9977ea346,21871a7f-f067-45ea-989e-44339bb5ad07,c3efedab-84f5-4656-8297-55964b3d26cb,647dcdd1-4540-4003-9f58-fd84d4d759b7,fc5e6857-8d67-47b8-98f2-edeb0671e326,1ad8d72c-1826-40ed-8b44-d15a1d2aab70,eac6c90a-d25d-4c8c-a053-cfbc7cde0afb,023a70de-a85e-43fc-bbc6-757fbf6562f0,f3f0a7fb-1409-443b-8e39-4e58e628796e,62828804-97d4-40ec-82fa-2992a6ce4a81,af5441fe-b16f-4996-87f4-1a433ec53dd6,e8857860-f7b1-4478-9741-1eb9e7c11f2c,6bca9c44-c8c0-49f8-b0b5-1bb2ca7842b8,d50da092-09df-448f-84ea-3ebddfe1d9f6,9efd5d6d-db64-47d4-9ad3-5e4d8b65ff7f,6f089094-2dd2-4b0e-b5b7-8bb52b93ea8e,299b0822-68e9-4bfa-af35-da799012e80e,a3dd579c-93be-45a0-ad35-f518d8ed45dd,023b1b3e-4891-4061-aae0-f34368644f40,50174445-33c5-4482-bb8c-3ef6c511c8cd,9933c048-07a7-4735-9af2-940c2f9b6683,beadc568-3962-46bd-ad4d-06e23b37615b,0cdafc9f-d4c1-4576-837e-d7f6ec28643d,50bb24ce-1709-4928-a87b-d9d9e147a2ab,7690ed72-910d-4357-8e0e-17aa702b0b94,1ad0d69f-60fa-414f-bf79-4f94c2abba43,946d84a4-db4d-48cb-a5d3-03081b5c7e8e,1a080055-d2bf-4b14-8957-88a7d08769b8,ed343e38-e405-4fae-9486-27b09c98bdad,c8bdef75-a98c-4646-a372-3251340d2dea,87a8c6fa-f93e-4950-aff2-5f956ca1a6ba,604781ba-23c2-4220-a717-b5615431fcd9,31af6841-ad9f-4f28-a637-b5c5e6589447,cf067451-7b88-4ff2-a96d-3fc9c5d6fea0,26a8ad5e-29d9-4e7d-aa1f-e6221e8ea32a,fd14db29-e4df-44a7-9b3f-d00384458122,73b477a8-fcf4-4860-a685-65a0a79b8653,82e0f305-4c6c-4160-be1d-b0de834124e6,e38567ab-a6e2-4a94-99c5-a7db31c0aae8,faf3d6dc-66ff-4c1b-9658-f65a9cd9fcf1,6df6bb90-200e-4290-b73d-9bb374554229,2ff10cf4-a871-404a-9e7b-5ca7a232567e,06c614e2-0f36-4b72-8c82-59631680add2,5e508c81-3453-4185-ae8c-4c9b841f8c15,21b5c371-6010-4b1b-be67-7538eb877efb,54e61442-e291-4eea-8d49-7f11b5f85bd2,b6b7260a-4eea-40b0-9f7f-1dfd4c3cc7a8,e163fe76-30fb-44fb-b51a-50cc78745a21,4da672f2-29b4-4a98-b27c-b39a4aecc858,2fdb0601-c882-4aaf-ad49-ae17e530d47a,49525e1b-1b47-4545-a98c-0ba58778179f,f958ab32-b152-4004-9228-18148f7380f1,0ff5025a-62cd-4a10-a722-79f7cf360f01,642df445-e314-409a-a97d-64fc2aa2a15e,38b0dab5-d4fb-44f9-8cf9-bb35cf82e91d,62054133-f35a-4f64-a2ee-a31e48952835,536dbe8c-af33-4eab-a0a8-8d039a00db40,a04998ba-52c9-4538-b6d9-6d04408dbaf2,89016c7a-3d36-4619-a5c6-4f31795eebf7,7708b9a9-776c-46fc-94a4-dc28e7880958,5c92bc69-b328-4c66-a791-a05dbaf7a6f8,ad580a50-80b4-44be-9bc4-f2b57cd23207,36c0241c-891e-4b74-bd10-5e99df96bbc8,a96842ff-18be-4536-a23d-20d973d91621,0ea549b0-9558-4bdf-9944-5abc707c7660,0186c353-5ed2-4c94-b71a-fc0b48c90288,1508a165-2217-4911-b31c-1ea42a08f097,1731e392-dfdf-4fc4-863b-27ae62b0e374,0b245cea-96a6-4a3a-af6a-ef43496c239c,a844e208-7078-43a2-8bd0-86f31498cd3f,53d112b5-87f2-490b-a788-df1f4624f9ad,0d5794d4-3a52-482b-9a6a-f8185018bad1,df877aa6-231c-47fb-9be0-906e61677356,c56c6d1a-3418-49d2-82ce-bd9370668043,6e0b6f34-3cd0-4aa0-ae1f-25f5545dca68
|
||||
MUTATION_MAXIMUM_RECORD_AFFECTED=100
|
||||
|
||||
AUTH_GOOGLE_ENABLED=false
|
||||
|
||||
@ -29,14 +29,9 @@ export class DataSeedDemoWorkspaceService {
|
||||
async seedDemo(): Promise<void> {
|
||||
try {
|
||||
await rawDataSource.initialize();
|
||||
const demoWorkspaceIds =
|
||||
this.environmentService.get('DEMO_WORKSPACE_IDS');
|
||||
|
||||
if (demoWorkspaceIds.length === 0) {
|
||||
throw new Error(
|
||||
'Could not get DEMO_WORKSPACE_IDS. Please specify in .env',
|
||||
);
|
||||
}
|
||||
// TODO: migrate demo seeds to dev seeds
|
||||
const demoWorkspaceIds = ['', ''];
|
||||
|
||||
await this.workspaceSchemaCache.flush();
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ export const seedUsers = async (
|
||||
'lastName',
|
||||
'email',
|
||||
'passwordHash',
|
||||
'canImpersonate',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
@ -31,6 +32,7 @@ export const seedUsers = async (
|
||||
email: 'tim@apple.dev',
|
||||
passwordHash:
|
||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||
canImpersonate: true,
|
||||
},
|
||||
{
|
||||
id: DEV_SEED_USER_IDS.JONY,
|
||||
@ -39,6 +41,7 @@ export const seedUsers = async (
|
||||
email: 'jony.ive@apple.dev',
|
||||
passwordHash:
|
||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||
canImpersonate: true,
|
||||
},
|
||||
{
|
||||
id: DEV_SEED_USER_IDS.PHIL,
|
||||
@ -47,6 +50,7 @@ export const seedUsers = async (
|
||||
email: 'phil.schiler@apple.dev',
|
||||
passwordHash:
|
||||
'$2b$10$66d.6DuQExxnrfI9rMqOg.U1XIYpagr6Lv05uoWLYbYmtK0HDIvS6', // Applecar2025
|
||||
canImpersonate: true,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
|
||||
@ -33,9 +33,33 @@ jest.mock(
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/environment/constants/environment-variables-hidden-groups',
|
||||
'src/engine/core-modules/environment/constants/environment-variables-group-metadata',
|
||||
() => ({
|
||||
ENVIRONMENT_VARIABLES_HIDDEN_GROUPS: new Set(['HIDDEN_GROUP']),
|
||||
ENVIRONMENT_VARIABLES_GROUP_METADATA: {
|
||||
GROUP_1: {
|
||||
position: 100,
|
||||
description: '',
|
||||
},
|
||||
GROUP_2: {
|
||||
position: 200,
|
||||
description: '',
|
||||
},
|
||||
VISIBLE_GROUP: {
|
||||
position: 300,
|
||||
description: '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'src/engine/core-modules/environment/constants/environment-variables-sub-group-metadata',
|
||||
() => ({
|
||||
ENVIRONMENT_VARIABLES_SUB_GROUP_METADATA: {
|
||||
SUBGROUP_1: {
|
||||
description: '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@ -262,9 +286,10 @@ describe('AdminPanelService', () => {
|
||||
const result = service.getEnvironmentVariablesGrouped();
|
||||
|
||||
expect(result).toEqual({
|
||||
groups: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
groupName: 'GROUP_1',
|
||||
groups: [
|
||||
{
|
||||
name: 'GROUP_1',
|
||||
description: '',
|
||||
variables: [
|
||||
{
|
||||
name: 'VAR_1',
|
||||
@ -275,7 +300,8 @@ describe('AdminPanelService', () => {
|
||||
],
|
||||
subgroups: [
|
||||
{
|
||||
subgroupName: 'SUBGROUP_1',
|
||||
name: 'SUBGROUP_1',
|
||||
description: '',
|
||||
variables: [
|
||||
{
|
||||
name: 'VAR_2',
|
||||
@ -286,9 +312,10 @@ describe('AdminPanelService', () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
groupName: 'GROUP_2',
|
||||
},
|
||||
{
|
||||
name: 'GROUP_2',
|
||||
description: '',
|
||||
variables: [
|
||||
{
|
||||
name: 'VAR_3',
|
||||
@ -298,8 +325,8 @@ describe('AdminPanelService', () => {
|
||||
},
|
||||
],
|
||||
subgroups: [],
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@ -324,7 +351,7 @@ describe('AdminPanelService', () => {
|
||||
const result = service.getEnvironmentVariablesGrouped();
|
||||
|
||||
const group = result.groups.find(
|
||||
(g) => g.groupName === ('GROUP_1' as EnvironmentVariablesGroup),
|
||||
(g) => g.name === ('GROUP_1' as EnvironmentVariablesGroup),
|
||||
);
|
||||
|
||||
expect(group?.variables[0].name).toBe('A_VAR');
|
||||
@ -340,34 +367,5 @@ describe('AdminPanelService', () => {
|
||||
groups: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude hidden groups from the output', () => {
|
||||
EnvironmentServiceGetAllMock.mockReturnValue({
|
||||
VAR_1: {
|
||||
value: 'value1',
|
||||
metadata: {
|
||||
group: 'HIDDEN_GROUP',
|
||||
description: 'Description 1',
|
||||
},
|
||||
},
|
||||
VAR_2: {
|
||||
value: 'value2',
|
||||
metadata: {
|
||||
group: 'VISIBLE_GROUP',
|
||||
description: 'Description 2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = service.getEnvironmentVariablesGrouped();
|
||||
|
||||
expect(result.groups).toHaveLength(1);
|
||||
expect(result.groups[0].groupName).toBe('VISIBLE_GROUP');
|
||||
expect(result.groups).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
groupName: 'HIDDEN_GROUP',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,8 +12,8 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
|
||||
import { ENVIRONMENT_VARIABLES_GROUP_POSITION } from 'src/engine/core-modules/environment/constants/environment-variables-group-position';
|
||||
import { ENVIRONMENT_VARIABLES_HIDDEN_GROUPS } from 'src/engine/core-modules/environment/constants/environment-variables-hidden-groups';
|
||||
import { ENVIRONMENT_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/environment/constants/environment-variables-group-metadata';
|
||||
import { ENVIRONMENT_VARIABLES_SUB_GROUP_METADATA } from 'src/engine/core-modules/environment/constants/environment-variables-sub-group-metadata';
|
||||
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
|
||||
import { EnvironmentVariablesSubGroup } from 'src/engine/core-modules/environment/enums/environment-variables-sub-group.enum';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
@ -180,10 +180,6 @@ export class AdminPanelService {
|
||||
for (const [varName, { value, metadata }] of Object.entries(rawEnvVars)) {
|
||||
const { group, subGroup, description } = metadata;
|
||||
|
||||
if (ENVIRONMENT_VARIABLES_HIDDEN_GROUPS.has(group)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const envVar: EnvironmentVariable = {
|
||||
name: varName,
|
||||
description,
|
||||
@ -218,18 +214,23 @@ export class AdminPanelService {
|
||||
groupedData.entries(),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const positionA = ENVIRONMENT_VARIABLES_GROUP_POSITION[a[0]];
|
||||
const positionB = ENVIRONMENT_VARIABLES_GROUP_POSITION[b[0]];
|
||||
const positionA = ENVIRONMENT_VARIABLES_GROUP_METADATA[a[0]].position;
|
||||
const positionB = ENVIRONMENT_VARIABLES_GROUP_METADATA[b[0]].position;
|
||||
|
||||
return positionA - positionB;
|
||||
})
|
||||
.map(([groupName, data]) => ({
|
||||
groupName,
|
||||
.map(([name, data]) => ({
|
||||
name,
|
||||
description: ENVIRONMENT_VARIABLES_GROUP_METADATA[name].description,
|
||||
isHiddenOnLoad:
|
||||
ENVIRONMENT_VARIABLES_GROUP_METADATA[name].isHiddenOnLoad,
|
||||
variables: data.variables.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
subgroups: Array.from(data.subgroups.entries())
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([subgroupName, variables]) => ({
|
||||
subgroupName,
|
||||
.map(([name, variables]) => ({
|
||||
name,
|
||||
description:
|
||||
ENVIRONMENT_VARIABLES_SUB_GROUP_METADATA[name].description,
|
||||
variables,
|
||||
})),
|
||||
}));
|
||||
|
||||
@ -18,5 +18,11 @@ export class EnvironmentVariablesGroupData {
|
||||
subgroups: EnvironmentVariablesSubgroupData[];
|
||||
|
||||
@Field(() => EnvironmentVariablesGroup)
|
||||
groupName: EnvironmentVariablesGroup;
|
||||
name: EnvironmentVariablesGroup;
|
||||
|
||||
@Field(() => String, { defaultValue: '' })
|
||||
description: string;
|
||||
|
||||
@Field(() => Boolean, { defaultValue: false })
|
||||
isHiddenOnLoad: boolean;
|
||||
}
|
||||
|
||||
@ -14,5 +14,8 @@ export class EnvironmentVariablesSubgroupData {
|
||||
variables: EnvironmentVariable[];
|
||||
|
||||
@Field(() => EnvironmentVariablesSubGroup)
|
||||
subgroupName: EnvironmentVariablesSubGroup;
|
||||
name: EnvironmentVariablesSubGroup;
|
||||
|
||||
@Field(() => String, { defaultValue: '' })
|
||||
description: string;
|
||||
}
|
||||
|
||||
@ -21,11 +21,11 @@ import { GoogleAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/gu
|
||||
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||
import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service';
|
||||
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
|
||||
@Controller('auth/google-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -72,16 +72,6 @@ export class GoogleAPIsAuthController {
|
||||
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 Google account to demo workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
|
||||
@ -23,9 +23,9 @@ import { TransientTokenService } from 'src/engine/core-modules/auth/token/servic
|
||||
import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
|
||||
@Controller('auth/microsoft-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
@ -72,16 +72,6 @@ export class MicrosoftAPIsAuthController {
|
||||
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',
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
import { ENVIRONMENT_VARIABLES_GROUP_POSITION } from 'src/engine/core-modules/environment/constants/environment-variables-group-position';
|
||||
import { ENVIRONMENT_VARIABLES_GROUP_METADATA } from 'src/engine/core-modules/environment/constants/environment-variables-group-metadata';
|
||||
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
|
||||
|
||||
describe('ENVIRONMENT_VARIABLES_GROUP_POSITION', () => {
|
||||
describe('ENVIRONMENT_VARIABLES_GROUP_METADATA', () => {
|
||||
it('should include all EnvironmentVariablesGroup enum values', () => {
|
||||
const enumValues = Object.values(EnvironmentVariablesGroup);
|
||||
const positionKeys = Object.keys(ENVIRONMENT_VARIABLES_GROUP_POSITION);
|
||||
const metadataKeys = Object.keys(ENVIRONMENT_VARIABLES_GROUP_METADATA);
|
||||
|
||||
enumValues.forEach((enumValue) => {
|
||||
expect(positionKeys).toContain(enumValue);
|
||||
expect(metadataKeys).toContain(enumValue);
|
||||
});
|
||||
|
||||
positionKeys.forEach((key) => {
|
||||
metadataKeys.forEach((key) => {
|
||||
expect(enumValues).toContain(key);
|
||||
});
|
||||
|
||||
expect(enumValues.length).toBe(positionKeys.length);
|
||||
expect(enumValues.length).toBe(metadataKeys.length);
|
||||
});
|
||||
|
||||
it('should have unique position values', () => {
|
||||
const positions = Object.values(ENVIRONMENT_VARIABLES_GROUP_POSITION);
|
||||
const positions = Object.values(ENVIRONMENT_VARIABLES_GROUP_METADATA).map(
|
||||
(metadata) => metadata.position,
|
||||
);
|
||||
const uniquePositions = new Set(positions);
|
||||
|
||||
expect(positions.length).toBe(uniquePositions.size);
|
||||
@ -0,0 +1,44 @@
|
||||
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
|
||||
|
||||
type GroupMetadata = {
|
||||
position: number;
|
||||
description: string;
|
||||
isHiddenOnLoad: boolean;
|
||||
};
|
||||
|
||||
export const ENVIRONMENT_VARIABLES_GROUP_METADATA: Record<
|
||||
EnvironmentVariablesGroup,
|
||||
GroupMetadata
|
||||
> = {
|
||||
[EnvironmentVariablesGroup.ServerConfig]: {
|
||||
position: 100,
|
||||
description: '',
|
||||
isHiddenOnLoad: false,
|
||||
},
|
||||
[EnvironmentVariablesGroup.Authentication]: {
|
||||
position: 400,
|
||||
description: '',
|
||||
isHiddenOnLoad: false,
|
||||
},
|
||||
[EnvironmentVariablesGroup.Email]: {
|
||||
position: 800,
|
||||
description: '',
|
||||
isHiddenOnLoad: false,
|
||||
},
|
||||
[EnvironmentVariablesGroup.Workspace]: {
|
||||
position: 1000,
|
||||
description: '',
|
||||
isHiddenOnLoad: false,
|
||||
},
|
||||
[EnvironmentVariablesGroup.Logging]: {
|
||||
position: 1200,
|
||||
description: '',
|
||||
isHiddenOnLoad: false,
|
||||
},
|
||||
[EnvironmentVariablesGroup.Other]: {
|
||||
position: 1700,
|
||||
description:
|
||||
"The variables in this section are mostly used for internal purposes (running our Cloud offering), but shouldn't usually be required for a simple self-hosted instance",
|
||||
isHiddenOnLoad: true,
|
||||
},
|
||||
};
|
||||
@ -1,23 +1,31 @@
|
||||
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
|
||||
|
||||
export const ENVIRONMENT_VARIABLES_GROUP_POSITION: Record<
|
||||
export const ENVIRONMENT_VARIABLES_GROUP_METADATA: Record<
|
||||
EnvironmentVariablesGroup,
|
||||
number
|
||||
{ position: number; description: string }
|
||||
> = {
|
||||
[EnvironmentVariablesGroup.ServerConfig]: 100,
|
||||
[EnvironmentVariablesGroup.Database]: 200,
|
||||
[EnvironmentVariablesGroup.Security]: 300,
|
||||
[EnvironmentVariablesGroup.Authentication]: 400,
|
||||
[EnvironmentVariablesGroup.Cache]: 500,
|
||||
[EnvironmentVariablesGroup.QueueConfig]: 600,
|
||||
[EnvironmentVariablesGroup.Storage]: 700,
|
||||
[EnvironmentVariablesGroup.Email]: 800,
|
||||
[EnvironmentVariablesGroup.Frontend]: 900,
|
||||
[EnvironmentVariablesGroup.Workspace]: 1000,
|
||||
[EnvironmentVariablesGroup.Analytics]: 1100,
|
||||
[EnvironmentVariablesGroup.Logging]: 1200,
|
||||
[EnvironmentVariablesGroup.Billing]: 1300,
|
||||
[EnvironmentVariablesGroup.Support]: 1400,
|
||||
[EnvironmentVariablesGroup.LLM]: 1500,
|
||||
[EnvironmentVariablesGroup.Serverless]: 1600,
|
||||
[EnvironmentVariablesGroup.ServerConfig]: {
|
||||
position: 100,
|
||||
description: '',
|
||||
},
|
||||
[EnvironmentVariablesGroup.Authentication]: {
|
||||
position: 400,
|
||||
description: '',
|
||||
},
|
||||
[EnvironmentVariablesGroup.Email]: {
|
||||
position: 800,
|
||||
description: '',
|
||||
},
|
||||
[EnvironmentVariablesGroup.Workspace]: {
|
||||
position: 1000,
|
||||
description: '',
|
||||
},
|
||||
[EnvironmentVariablesGroup.Logging]: {
|
||||
position: 1200,
|
||||
description: '',
|
||||
},
|
||||
[EnvironmentVariablesGroup.Other]: {
|
||||
position: 1700,
|
||||
description: '',
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
|
||||
|
||||
export const ENVIRONMENT_VARIABLES_HIDDEN_GROUPS: Set<EnvironmentVariablesGroup> =
|
||||
new Set([EnvironmentVariablesGroup.LLM]);
|
||||
@ -0,0 +1,71 @@
|
||||
import { EnvironmentVariablesSubGroup } from 'src/engine/core-modules/environment/enums/environment-variables-sub-group.enum';
|
||||
|
||||
type SubGroupMetadata = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const ENVIRONMENT_VARIABLES_SUB_GROUP_METADATA: Record<
|
||||
EnvironmentVariablesSubGroup,
|
||||
SubGroupMetadata
|
||||
> = {
|
||||
[EnvironmentVariablesSubGroup.PasswordAuth]: {
|
||||
description: '',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.GoogleAuth]: {
|
||||
description: 'Configure Google integration (login, calendar, email)',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.MicrosoftAuth]: {
|
||||
description: 'Configure Microsoft integration (login, calendar, email)',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.EmailSettings]: {
|
||||
description:
|
||||
'This is used for emails that are sent by the app such as invitations to join a workspace. This is not used to email CRM contacts.',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.StorageConfig]: {
|
||||
description:
|
||||
"By default, file uploads are stored on the local filesystem, which is suitable for traditional servers. However, for ephemeral deployment servers, it's essential to configure the variables here to set up an S3-compatible file system. This ensures that files remain unaffected by server redeploys.",
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.TokensDuration]: {
|
||||
description:
|
||||
"These have been set to sensible default so you probably don't need to change them unless you have a specific use-case.",
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.SSL]: {
|
||||
description:
|
||||
'Configure this if you want to setup SSL on your server or full end-to-end encryption. If you just want basic HTTPS, a simple setup like Cloudflare in flexible mode might be easier.',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.RateLimiting]: {
|
||||
description:
|
||||
'We use this to limit the number of requests to the server. This is useful to prevent abuse.',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.TinybirdConfig]: {
|
||||
description:
|
||||
"We're running a test to perform analytics within the app. This will evolve.",
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.BillingConfig]: {
|
||||
description:
|
||||
'We use Stripe in our Cloud app to charge customers. Not relevant to Self-hosters.',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.ExceptionHandler]: {
|
||||
description:
|
||||
'By default, exceptions are sent to the logs. This should be enough for most self-hosting use-cases. For our cloud app we use Sentry.',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.SupportChatConfig]: {
|
||||
description:
|
||||
'We use this to setup a small support chat on the bottom left. Currently powered by Front.',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.CloudflareConfig]: {
|
||||
description: '',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.CaptchaConfig]: {
|
||||
description:
|
||||
'This protects critical endpoints like login and signup with a captcha to prevent bot attacks. Likely unnecessary for self-hosting scenarios.',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.ServerlessConfig]: {
|
||||
description:
|
||||
'In our multi-tenant cloud app, we offload untrusted custom code from workflows to a serverless system (Lambda) for enhanced security and scalability. Self-hosters with a single tenant can typically ignore this configuration.',
|
||||
},
|
||||
[EnvironmentVariablesSubGroup.LLM]: {
|
||||
description:
|
||||
'Configure the LLM provider and model to use for the app. This is experimental and not linked to any public feature.',
|
||||
},
|
||||
};
|
||||
@ -1,18 +1,8 @@
|
||||
export enum EnvironmentVariablesGroup {
|
||||
Authentication = 'authentication',
|
||||
Email = 'email',
|
||||
Database = 'database',
|
||||
Storage = 'storage',
|
||||
ServerConfig = 'server-config',
|
||||
QueueConfig = 'queue-config',
|
||||
Logging = 'logging',
|
||||
Cache = 'cache',
|
||||
Analytics = 'analytics',
|
||||
Billing = 'billing',
|
||||
Frontend = 'frontend',
|
||||
Security = 'security',
|
||||
Serverless = 'serverless',
|
||||
Support = 'support',
|
||||
LLM = 'llm',
|
||||
Workspace = 'workspace',
|
||||
Other = 'other',
|
||||
}
|
||||
|
||||
@ -2,16 +2,17 @@ export enum EnvironmentVariablesSubGroup {
|
||||
PasswordAuth = 'password-auth',
|
||||
GoogleAuth = 'google-auth',
|
||||
MicrosoftAuth = 'microsoft-auth',
|
||||
SmtpConfig = 'smtp-config',
|
||||
EmailSettings = 'email-settings',
|
||||
S3Config = 's3-config',
|
||||
Tokens = 'tokens',
|
||||
StorageConfig = 'storage-config',
|
||||
TokensDuration = 'tokens-duration',
|
||||
SSL = 'ssl',
|
||||
RateLimiting = 'rate-limiting',
|
||||
LambdaConfig = 'lambda-config',
|
||||
TinybirdConfig = 'tinybird-config',
|
||||
StripeConfig = 'stripe-config',
|
||||
SentryConfig = 'sentry-config',
|
||||
FrontSupportConfig = 'front-support-config',
|
||||
BillingConfig = 'billing-config',
|
||||
ExceptionHandler = 'exception-handler',
|
||||
SupportChatConfig = 'support-chat-config',
|
||||
CloudflareConfig = 'cloudflare-config',
|
||||
CaptchaConfig = 'captcha-config',
|
||||
ServerlessConfig = 'serverless-config',
|
||||
LLM = 'llm',
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -86,26 +86,5 @@ describe('EnvironmentService', () => {
|
||||
expect(result.APP_SECRET.value).not.toBe('super-secret-value');
|
||||
expect(result.APP_SECRET.value).toMatch(/^\*+[a-zA-Z0-9]{5}$/);
|
||||
});
|
||||
|
||||
it('should use default value when environment variable is not set', () => {
|
||||
const mockMetadata = {
|
||||
DEBUG_PORT: {
|
||||
title: 'Debug Port',
|
||||
description: 'Debug port number',
|
||||
},
|
||||
};
|
||||
|
||||
Reflect.defineMetadata(
|
||||
'environment-variables',
|
||||
mockMetadata,
|
||||
EnvironmentVariables,
|
||||
);
|
||||
|
||||
jest.spyOn(configService, 'get').mockReturnValue(undefined);
|
||||
|
||||
const result = service.getAll();
|
||||
|
||||
expect(result.DEBUG_PORT.value).toBe(9000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,11 +8,10 @@ import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, DemoEnvGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@Resolver()
|
||||
export class FileUploadResolver {
|
||||
constructor(private readonly fileUploadService: FileUploadService) {}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { OnModuleDestroy } from '@nestjs/common';
|
||||
|
||||
import { JobsOptions, Queue, QueueOptions, Worker } from 'bullmq';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
QueueCronJobOptions,
|
||||
|
||||
@ -46,7 +46,6 @@ import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
@ -295,7 +294,6 @@ export class UserResolver {
|
||||
return `${paths[0]}?token=${fileToken}`;
|
||||
}
|
||||
|
||||
@UseGuards(DemoEnvGuard)
|
||||
@Mutation(() => User)
|
||||
async deleteUser(@AuthUser() { id: userId }: User) {
|
||||
// Proceed with user deletion
|
||||
|
||||
@ -10,8 +10,8 @@ import {
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
@ -39,7 +39,6 @@ import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
|
||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { GraphqlValidationExceptionFilter } from 'src/filters/graphql-validation-exception.filter';
|
||||
@ -151,7 +150,7 @@ export class WorkspaceResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => Workspace)
|
||||
@UseGuards(DemoEnvGuard, WorkspaceAuthGuard)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
async deleteCurrentWorkspace(@AuthWorkspace() { id }: Workspace) {
|
||||
return this.workspaceService.deleteWorkspace(id);
|
||||
}
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class DemoEnvGuard implements CanActivate {
|
||||
constructor(private readonly environmentService: EnvironmentService) {}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
const request = ctx.getContext().req;
|
||||
|
||||
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');
|
||||
const currentUserWorkspaceId = request.workspace?.id;
|
||||
|
||||
if (!currentUserWorkspaceId) {
|
||||
throw new UnauthorizedException('Unauthorized for not logged in user');
|
||||
}
|
||||
|
||||
if (demoWorkspaceIds.includes(currentUserWorkspaceId)) {
|
||||
throw new UnauthorizedException('Unauthorized for demo workspace');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,9 @@ export {
|
||||
IconArrowDown,
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconArrowsVertical,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
IconArrowsVertical,
|
||||
IconAt,
|
||||
IconBaselineDensitySmall,
|
||||
IconBell,
|
||||
@ -40,8 +40,8 @@ export {
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconChevronUp,
|
||||
IconChevronsRight,
|
||||
IconChevronUp,
|
||||
IconCircleDot,
|
||||
IconCircleOff,
|
||||
IconCirclePlus,
|
||||
@ -227,6 +227,7 @@ export {
|
||||
IconSend,
|
||||
IconServer,
|
||||
IconSettings,
|
||||
IconSettings2,
|
||||
IconSettingsAutomation,
|
||||
IconSlash,
|
||||
IconSortAZ,
|
||||
|
||||
Reference in New Issue
Block a user