Environment variables in admin panel (read only) - backend (#9943)

Backend 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.malfait@gmail.com>
This commit is contained in:
nitin
2025-02-03 21:05:43 +05:30
committed by GitHub
parent 49e4484937
commit c8af90dc01
25 changed files with 1827 additions and 374 deletions

View File

@ -7,6 +7,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 { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
@ -17,6 +19,7 @@ const WorkspaceFindOneMock = jest.fn();
const FeatureFlagUpdateMock = jest.fn();
const FeatureFlagSaveMock = jest.fn();
const LoginTokenServiceGenerateLoginTokenMock = jest.fn();
const EnvironmentServiceGetAllMock = jest.fn();
jest.mock(
'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum',
@ -29,6 +32,13 @@ jest.mock(
},
);
jest.mock(
'src/engine/core-modules/environment/constants/environment-variables-hidden-groups',
() => ({
ENVIRONMENT_VARIABLES_HIDDEN_GROUPS: new Set(['HIDDEN_GROUP']),
}),
);
describe('AdminPanelService', () => {
let service: AdminPanelService;
@ -61,6 +71,12 @@ describe('AdminPanelService', () => {
generateLoginToken: LoginTokenServiceGenerateLoginTokenMock,
},
},
{
provide: EnvironmentService,
useValue: {
getAll: EnvironmentServiceGetAllMock,
},
},
],
}).compile();
@ -214,4 +230,144 @@ describe('AdminPanelService', () => {
expect(UserFindOneMock).toHaveBeenCalled();
});
describe('getEnvironmentVariablesGrouped', () => {
it('should correctly group environment variables', () => {
EnvironmentServiceGetAllMock.mockReturnValue({
VAR_1: {
value: 'value1',
metadata: {
group: 'GROUP_1',
description: 'Description 1',
},
},
VAR_2: {
value: 'value2',
metadata: {
group: 'GROUP_1',
subGroup: 'SUBGROUP_1',
description: 'Description 2',
sensitive: true,
},
},
VAR_3: {
value: 'value3',
metadata: {
group: 'GROUP_2',
description: 'Description 3',
},
},
});
const result = service.getEnvironmentVariablesGrouped();
expect(result).toEqual({
groups: expect.arrayContaining([
expect.objectContaining({
groupName: 'GROUP_1',
variables: [
{
name: 'VAR_1',
value: 'value1',
description: 'Description 1',
sensitive: false,
},
],
subgroups: [
{
subgroupName: 'SUBGROUP_1',
variables: [
{
name: 'VAR_2',
value: 'value2',
description: 'Description 2',
sensitive: true,
},
],
},
],
}),
expect.objectContaining({
groupName: 'GROUP_2',
variables: [
{
name: 'VAR_3',
value: 'value3',
description: 'Description 3',
sensitive: false,
},
],
subgroups: [],
}),
]),
});
});
it('should sort groups by position and variables alphabetically', () => {
EnvironmentServiceGetAllMock.mockReturnValue({
Z_VAR: {
value: 'valueZ',
metadata: {
group: 'GROUP_1',
description: 'Description Z',
},
},
A_VAR: {
value: 'valueA',
metadata: {
group: 'GROUP_1',
description: 'Description A',
},
},
});
const result = service.getEnvironmentVariablesGrouped();
const group = result.groups.find(
(g) => g.groupName === ('GROUP_1' as EnvironmentVariablesGroup),
);
expect(group?.variables[0].name).toBe('A_VAR');
expect(group?.variables[1].name).toBe('Z_VAR');
});
it('should handle empty environment variables', () => {
EnvironmentServiceGetAllMock.mockReturnValue({});
const result = service.getEnvironmentVariablesGrouped();
expect(result).toEqual({
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

@ -1,7 +1,8 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
import { EnvironmentVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/environment-variables.output';
import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input';
import { ImpersonateOutput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.output';
import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input';
@ -46,4 +47,10 @@ export class AdminPanelResolver {
return true;
}
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
@Query(() => EnvironmentVariablesOutput)
async getEnvironmentVariablesGrouped(): Promise<EnvironmentVariablesOutput> {
return this.adminService.getEnvironmentVariablesGrouped();
}
}

View File

@ -3,12 +3,20 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EnvironmentVariable } from 'src/engine/core-modules/admin-panel/dtos/environment-variable.dto';
import { EnvironmentVariablesGroupData } from 'src/engine/core-modules/admin-panel/dtos/environment-variables-group.dto';
import { EnvironmentVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/environment-variables.output';
import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { 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 { 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';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import {
@ -25,6 +33,7 @@ import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.
export class AdminPanelService {
constructor(
private readonly loginTokenService: LoginTokenService,
private readonly environmentService: EnvironmentService,
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
@InjectRepository(Workspace, 'core')
@ -157,4 +166,74 @@ export class AdminPanelService {
});
}
}
getEnvironmentVariablesGrouped(): EnvironmentVariablesOutput {
const rawEnvVars = this.environmentService.getAll();
const groupedData = new Map<
EnvironmentVariablesGroup,
{
variables: EnvironmentVariable[];
subgroups: Map<EnvironmentVariablesSubGroup, EnvironmentVariable[]>;
}
>();
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,
value: String(value),
sensitive: metadata.sensitive ?? false,
};
let currentGroup = groupedData.get(group);
if (!currentGroup) {
currentGroup = {
variables: [],
subgroups: new Map(),
};
groupedData.set(group, currentGroup);
}
if (subGroup) {
let subgroupVars = currentGroup.subgroups.get(subGroup);
if (!subgroupVars) {
subgroupVars = [];
currentGroup.subgroups.set(subGroup, subgroupVars);
}
subgroupVars.push(envVar);
} else {
currentGroup.variables.push(envVar);
}
}
const groups: EnvironmentVariablesGroupData[] = Array.from(
groupedData.entries(),
)
.sort((a, b) => {
const positionA = ENVIRONMENT_VARIABLES_GROUP_POSITION[a[0]];
const positionB = ENVIRONMENT_VARIABLES_GROUP_POSITION[b[0]];
return positionA - positionB;
})
.map(([groupName, data]) => ({
groupName,
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,
variables,
})),
}));
return { groups };
}
}

View File

@ -0,0 +1,16 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class EnvironmentVariable {
@Field()
name: string;
@Field()
description: string;
@Field()
value: string;
@Field()
sensitive: boolean;
}

View File

@ -0,0 +1,22 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { EnvironmentVariablesGroup } from 'src/engine/core-modules/environment/enums/environment-variables-group.enum';
import { EnvironmentVariable } from './environment-variable.dto';
import { EnvironmentVariablesSubgroupData } from './environment-variables-subgroup.dto';
registerEnumType(EnvironmentVariablesGroup, {
name: 'EnvironmentVariablesGroup',
});
@ObjectType()
export class EnvironmentVariablesGroupData {
@Field(() => [EnvironmentVariable])
variables: EnvironmentVariable[];
@Field(() => [EnvironmentVariablesSubgroupData])
subgroups: EnvironmentVariablesSubgroupData[];
@Field(() => EnvironmentVariablesGroup)
groupName: EnvironmentVariablesGroup;
}

View File

@ -0,0 +1,18 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { EnvironmentVariablesSubGroup } from 'src/engine/core-modules/environment/enums/environment-variables-sub-group.enum';
import { EnvironmentVariable } from './environment-variable.dto';
registerEnumType(EnvironmentVariablesSubGroup, {
name: 'EnvironmentVariablesSubGroup',
});
@ObjectType()
export class EnvironmentVariablesSubgroupData {
@Field(() => [EnvironmentVariable])
variables: EnvironmentVariable[];
@Field(() => EnvironmentVariablesSubGroup)
subgroupName: EnvironmentVariablesSubGroup;
}

View File

@ -0,0 +1,9 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { EnvironmentVariablesGroupData } from './environment-variables-group.dto';
@ObjectType()
export class EnvironmentVariablesOutput {
@Field(() => [EnvironmentVariablesGroupData])
groups: EnvironmentVariablesGroupData[];
}