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:
nitin
2025-02-06 21:38:44 +05:30
committed by GitHub
parent a85c4f263a
commit 1b150e1da6
43 changed files with 1224 additions and 758 deletions

View File

@ -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 */

View File

@ -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

View File

@ -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 />
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
</>
);
};

View File

@ -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>
);

View File

@ -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>
)}
</>
);
};

View File

@ -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;
}
};

View File

@ -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>
)}
</>
);
};

View File

@ -1,2 +0,0 @@
export const SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID =
'settings-admin-feature-flags-tab-id';

View File

@ -0,0 +1,4 @@
export const SETTINGS_ADMIN_TABS = {
GENERAL: 'general',
ENV_VARIABLES: 'env-variables',
};

View File

@ -0,0 +1 @@
export const SETTINGS_ADMIN_TABS_ID = 'settings-admin-tabs-id';

View File

@ -0,0 +1,2 @@
export const SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID =
'settings-admin-user-lookup-workspace-tabs-id';

View File

@ -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

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const adminPanelErrorState = atom<string | null>({
key: 'adminPanelErrorState',
default: null,
});

View File

@ -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,
});

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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',
}),
);
});
});
});

View File

@ -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,
})),
}));

View File

@ -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;
}

View File

@ -14,5 +14,8 @@ export class EnvironmentVariablesSubgroupData {
variables: EnvironmentVariable[];
@Field(() => EnvironmentVariablesSubGroup)
subgroupName: EnvironmentVariablesSubGroup;
name: EnvironmentVariablesSubGroup;
@Field(() => String, { defaultValue: '' })
description: string;
}

View File

@ -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',

View File

@ -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',

View File

@ -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);

View File

@ -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,
},
};

View File

@ -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: '',
},
};

View File

@ -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]);

View File

@ -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.',
},
};

View File

@ -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',
}

View File

@ -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',
}

View File

@ -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);
});
});
});

View File

@ -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) {}

View File

@ -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,

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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,