diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts index b1fed90bd..ed0be33d9 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { isDefined } from 'class-validator'; + import { ForeignDataWrapperOptions, RemoteServerType, @@ -21,6 +23,20 @@ export class ForeignDataWrapperQueryFactory { return `CREATE SERVER "${foreignDataWrapperId}" FOREIGN DATA WRAPPER ${name} OPTIONS (${options})`; } + updateForeignDataWrapper({ + foreignDataWrapperId, + foreignDataWrapperOptions, + }: { + foreignDataWrapperId: string; + foreignDataWrapperOptions: Partial< + ForeignDataWrapperOptions + >; + }) { + const options = this.buildUpdateOptions(foreignDataWrapperOptions); + + return `ALTER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`; + } + createUserMapping( foreignDataWrapperId: string, userMappingOptions: UserMappingOptions, @@ -29,6 +45,16 @@ export class ForeignDataWrapperQueryFactory { return `CREATE USER MAPPING IF NOT EXISTS FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (user '${userMappingOptions.username}', password '${userMappingOptions.password}')`; } + updateUserMapping( + foreignDataWrapperId: string, + userMappingOptions: Partial, + ) { + const options = this.buildUpdateUserMappingOptions(userMappingOptions); + + // CURRENT_USER works for now since we are using only one user. But if we switch to a user per workspace, we need to change this. + return `ALTER USER MAPPING FOR CURRENT_USER SERVER "${foreignDataWrapperId}" OPTIONS (${options})`; + } + private buildNameAndOptionsFromType( type: RemoteServerType, options: ForeignDataWrapperOptions, @@ -41,6 +67,36 @@ export class ForeignDataWrapperQueryFactory { } } + private buildUpdateOptions( + options: Partial>, + ) { + const rawQuerySetStatements: string[] = []; + + Object.entries(options).forEach(([key, value]) => { + if (isDefined(value)) { + rawQuerySetStatements.push(`SET ${key} '${value}'`); + } + }); + + return rawQuerySetStatements.join(', '); + } + + private buildUpdateUserMappingOptions( + userMappingOptions?: Partial, + ) { + const setStatements: string[] = []; + + if (isDefined(userMappingOptions?.username)) { + setStatements.push(`SET user '${userMappingOptions?.username}'`); + } + + if (isDefined(userMappingOptions?.password)) { + setStatements.push(`SET password '${userMappingOptions?.password}'`); + } + + return setStatements.join(', '); + } + private buildPostgresFDWQueryOptions( foreignDataWrapperOptions: ForeignDataWrapperOptions, ) { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index e0f3db4de..22498783e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -39,8 +39,8 @@ import { import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { validateString } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input'; import { InvalidStringException } from 'src/engine/metadata-modules/errors/InvalidStringException'; +import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import { FieldMetadataEntity, @@ -543,7 +543,7 @@ export class FieldMetadataService extends TypeOrmQueryService(fieldMetadataInput: T): T { if (fieldMetadataInput.name) { try { - validateString(fieldMetadataInput.name); + validateMetadataName(fieldMetadataInput.name); } catch (error) { if (error instanceof InvalidStringException) { throw new BadRequestException( diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts index 35cacf89b..64dcccaf9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/create-remote-server.input.ts @@ -8,17 +8,17 @@ import { RemoteServerType, UserMappingOptions, } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { UserMappingOptionsInput } from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options-input.utils'; @InputType() export class CreateRemoteServerInput { @Field(() => String) foreignDataWrapperType: T; - @IsOptional() @Field(() => GraphQLJSON) foreignDataWrapperOptions: ForeignDataWrapperOptions; @IsOptional() - @Field(() => GraphQLJSON, { nullable: true }) + @Field(() => UserMappingOptionsInput, { nullable: true }) userMappingOptions?: UserMappingOptions; } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/update-remote-server.input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/update-remote-server.input.ts new file mode 100644 index 000000000..0ee527b92 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/dtos/update-remote-server.input.ts @@ -0,0 +1,25 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { IsOptional } from 'class-validator'; +import GraphQLJSON from 'graphql-type-json'; + +import { + ForeignDataWrapperOptions, + RemoteServerType, + UserMappingOptions, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; +import { UserMappingOptionsInput } from 'src/engine/metadata-modules/remote-server/utils/user-mapping-options-input.utils'; + +@InputType() +export class UpdateRemoteServerInput { + @Field(() => String) + id: string; + + @IsOptional() + @Field(() => GraphQLJSON, { nullable: true }) + foreignDataWrapperOptions?: Partial>; + + @IsOptional() + @Field(() => UserMappingOptionsInput, { nullable: true }) + userMappingOptions?: Partial; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts index 9fc4bdbcb..1fb0dc74c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.module.ts @@ -6,11 +6,13 @@ import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/re import { RemoteServerResolver } from 'src/engine/metadata-modules/remote-server/remote-server.resolver'; import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service'; import { RemoteTableModule } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @Module({ imports: [ TypeOrmModule.forFeature([RemoteServerEntity], 'metadata'), RemoteTableModule, + WorkspaceDataSourceModule, ], providers: [ RemoteServerService, diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts index 9165aa408..04d44a190 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.resolver.ts @@ -8,6 +8,7 @@ import { CreateRemoteServerInput } from 'src/engine/metadata-modules/remote-serv import { RemoteServerIdInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-id.input'; import { RemoteServerTypeInput } from 'src/engine/metadata-modules/remote-server/dtos/remote-server-type.input'; import { RemoteServerDTO } from 'src/engine/metadata-modules/remote-server/dtos/remote-server.dto'; +import { UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input'; import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service'; @@ -26,6 +27,14 @@ export class RemoteServerResolver { return this.remoteServerService.createOneRemoteServer(input, workspaceId); } + @Mutation(() => RemoteServerDTO) + async updateOneRemoteServer( + @Args('input') input: UpdateRemoteServerInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.remoteServerService.updateOneRemoteServer(input, workspaceId); + } + @Mutation(() => RemoteServerDTO) async deleteOneRemoteServer( @Args('input') { id }: RemoteServerIdInput, diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index 0a1cc88e2..fa9d10204 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { v4 } from 'uuid'; @@ -12,11 +16,14 @@ import { import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { encryptText } from 'src/engine/core-modules/auth/auth.util'; import { - validateObject, - validateString, -} from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input'; + validateObjectAgainstInjections, + validateStringAgainstInjections, +} from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.utils'; import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory'; import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service'; +import { UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; +import { updateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils'; @Injectable() export class RemoteServerService { @@ -30,17 +37,14 @@ export class RemoteServerService { private readonly environmentService: EnvironmentService, private readonly foreignDataWrapperQueryFactory: ForeignDataWrapperQueryFactory, private readonly remoteTableService: RemoteTableService, + private readonly workspaceDataSourceService: WorkspaceDataSourceService, ) {} async createOneRemoteServer( remoteServerInput: CreateRemoteServerInput, workspaceId: string, ): Promise> { - validateObject(remoteServerInput.foreignDataWrapperOptions); - - if (remoteServerInput.userMappingOptions) { - validateObject(remoteServerInput.userMappingOptions); - } + this.validateRemoteServerInputAgainstInjections(remoteServerInput); const foreignDataWrapperId = v4(); @@ -51,24 +55,20 @@ export class RemoteServerService { }; if (remoteServerInput.userMappingOptions) { - const key = this.environmentService.get('LOGIN_TOKEN_SECRET'); - const encryptedPassword = await encryptText( - remoteServerInput.userMappingOptions.password, - key, - ); - remoteServerToCreate = { ...remoteServerToCreate, userMappingOptions: { ...remoteServerInput.userMappingOptions, - password: encryptedPassword, + password: this.encryptPassword( + remoteServerInput.userMappingOptions.password, + ), }, }; } return this.metadataDataSource.transaction( async (entityManager: EntityManager) => { - const createdRemoteServer = await entityManager.create( + const createdRemoteServer = entityManager.create( RemoteServerEntity, remoteServerToCreate, ); @@ -99,11 +99,104 @@ export class RemoteServerService { ); } + async updateOneRemoteServer( + remoteServerInput: UpdateRemoteServerInput, + workspaceId: string, + ): Promise> { + this.validateRemoteServerInputAgainstInjections(remoteServerInput); + + const remoteServer = await this.findOneByIdWithinWorkspace( + remoteServerInput.id, + workspaceId, + ); + + if (!remoteServer) { + throw new NotFoundException('Remote server does not exist'); + } + + const currentRemoteTablesForServer = + await this.remoteTableService.findCurrentRemoteTablesByServerId({ + remoteServerId: remoteServer.id, + workspaceId, + }); + + if (currentRemoteTablesForServer.length > 0) { + throw new ForbiddenException( + 'Cannot update remote server with synchronized tables', + ); + } + + const foreignDataWrapperId = remoteServer.foreignDataWrapperId; + + let partialRemoteServerWithUpdates = { + ...remoteServerInput, + workspaceId, + foreignDataWrapperId, + }; + + if (partialRemoteServerWithUpdates?.userMappingOptions?.password) { + partialRemoteServerWithUpdates = { + ...partialRemoteServerWithUpdates, + userMappingOptions: { + ...partialRemoteServerWithUpdates.userMappingOptions, + password: this.encryptPassword( + partialRemoteServerWithUpdates.userMappingOptions.password, + ), + }, + }; + } + + return this.metadataDataSource.transaction( + async (entityManager: EntityManager) => { + const updatedRemoteServer = await this.updateRemoteServer( + partialRemoteServerWithUpdates, + ); + + if (partialRemoteServerWithUpdates.foreignDataWrapperOptions) { + const foreignDataWrapperQuery = + this.foreignDataWrapperQueryFactory.updateForeignDataWrapper({ + foreignDataWrapperId, + foreignDataWrapperOptions: + partialRemoteServerWithUpdates.foreignDataWrapperOptions, + }); + + await entityManager.query(foreignDataWrapperQuery); + } + + if (partialRemoteServerWithUpdates.userMappingOptions) { + const userMappingQuery = + this.foreignDataWrapperQueryFactory.updateUserMapping( + foreignDataWrapperId, + partialRemoteServerWithUpdates.userMappingOptions, + ); + + await entityManager.query(userMappingQuery); + } + + return updatedRemoteServer; + }, + ); + } + + private validateRemoteServerInputAgainstInjections( + remoteServerInput: CreateRemoteServerInput | UpdateRemoteServerInput, + ) { + if (remoteServerInput.foreignDataWrapperOptions) { + validateObjectAgainstInjections( + remoteServerInput.foreignDataWrapperOptions, + ); + } + + if (remoteServerInput.userMappingOptions) { + validateObjectAgainstInjections(remoteServerInput.userMappingOptions); + } + } + async deleteOneRemoteServer( id: string, workspaceId: string, ): Promise> { - validateString(id); + validateStringAgainstInjections(id); const remoteServer = await this.remoteServerRepository.findOne({ where: { @@ -150,4 +243,26 @@ export class RemoteServerService { }, }); } + + private encryptPassword(password: string) { + const key = this.environmentService.get('LOGIN_TOKEN_SECRET'); + + return encryptText(password, key); + } + + private async updateRemoteServer( + remoteServerToUpdate: DeepPartial> & + Pick, 'workspaceId' | 'id'>, + ): Promise> { + const [parameters, rawQuery] = + updateRemoteServerRawQuery(remoteServerToUpdate); + + const updateResult = await this.workspaceDataSourceService.executeRawQuery( + rawQuery, + parameters, + remoteServerToUpdate.workspaceId, + ); + + return updateResult[0][0]; + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts index dca660007..24e0728b3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-table/remote-table.service.ts @@ -74,14 +74,14 @@ export class RemoteTableService { throw new NotFoundException('Remote server does not exist'); } - const currentRemoteTableDistantNames = ( - await this.remoteTableRepository.find({ - where: { - remoteServerId: id, - workspaceId, - }, - }) - ).map((remoteTable) => remoteTable.distantTableName); + const currentRemoteTables = await this.findCurrentRemoteTablesByServerId({ + remoteServerId: id, + workspaceId, + }); + + const currentRemoteTableDistantNames = currentRemoteTables.map( + (remoteTable) => remoteTable.distantTableName, + ); const tablesInRemoteSchema = await this.fetchTablesFromRemoteSchema(remoteServer); @@ -95,6 +95,21 @@ export class RemoteTableService { })); } + public async findCurrentRemoteTablesByServerId({ + remoteServerId, + workspaceId, + }: { + remoteServerId: string; + workspaceId: string; + }) { + return this.remoteTableRepository.find({ + where: { + remoteServerId, + workspaceId, + }, + }); + } + public async syncRemoteTable(input: RemoteTableInput, workspaceId: string) { if (!input.schema) { throw new BadRequestException( diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils.ts new file mode 100644 index 000000000..435751d5e --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils.ts @@ -0,0 +1,138 @@ +import { isDefined } from 'class-validator'; + +import { + RemoteServerEntity, + RemoteServerType, + UserMappingOptions, +} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; + +export type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; + +const buildUserMappingOptionsQuery = ( + parameters: any[], + parametersPositions: object, + userMappingOptions: DeepPartial, +): string | null => { + const shouldUpdateUserMappingOptionsPassword = isDefined( + userMappingOptions?.password, + ); + + if (shouldUpdateUserMappingOptionsPassword) { + parameters.push(userMappingOptions?.password); + parametersPositions['password'] = parameters.length; + } + + const shouldUpdateUserMappingOptionsUsername = isDefined( + userMappingOptions?.username, + ); + + if (shouldUpdateUserMappingOptionsUsername) { + parameters.push(userMappingOptions?.username); + parametersPositions['username'] = parameters.length; + } + + if ( + shouldUpdateUserMappingOptionsPassword || + shouldUpdateUserMappingOptionsUsername + ) { + return `"userMappingOptions" = jsonb_set(${ + shouldUpdateUserMappingOptionsPassword && + shouldUpdateUserMappingOptionsUsername + ? `jsonb_set( + "userMappingOptions", + '{username}', + to_jsonb($${parametersPositions['username']}::text) + ), + '{password}', + to_jsonb($${parametersPositions['password']}::text) + ` + : shouldUpdateUserMappingOptionsPassword + ? `"userMappingOptions", + '{password}', + to_jsonb($${parametersPositions['password']}::text) + ` + : `"userMappingOptions", + '{username}', + to_jsonb($${parametersPositions['username']}::text) + ` + })`; + } + + return null; +}; + +// TO DO This only works for postgres_fdw type for now, lets make it more generic when we have a different type +export const updateRemoteServerRawQuery = ( + remoteServerToUpdate: DeepPartial> & + Pick, 'workspaceId' | 'id'>, +): [any[], string] => { + const parameters: any[] = [remoteServerToUpdate.id]; + const parametersPositions = {}; + + const options: string[] = []; + + if (remoteServerToUpdate.userMappingOptions) { + const userMappingOptionsQuery = buildUserMappingOptionsQuery( + parameters, + parametersPositions, + remoteServerToUpdate.userMappingOptions, + ); + + if (userMappingOptionsQuery) options.push(userMappingOptionsQuery); + } + + const shouldUpdateFdwDbname = isDefined( + remoteServerToUpdate.foreignDataWrapperOptions?.dbname, + ); + + if (shouldUpdateFdwDbname) { + parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.dbname); + parametersPositions['dbname'] = parameters.length; + } + + const shouldUpdateFdwHost = isDefined( + remoteServerToUpdate.foreignDataWrapperOptions?.host, + ); + + if (shouldUpdateFdwHost) { + parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.host); + parametersPositions['host'] = parameters.length; + } + + const shouldUpdateFdwPort = isDefined( + remoteServerToUpdate.foreignDataWrapperOptions?.port, + ); + + if (shouldUpdateFdwPort) { + parameters.push(remoteServerToUpdate?.foreignDataWrapperOptions?.port); + parametersPositions['port'] = parameters.length; + } + + if (shouldUpdateFdwDbname || shouldUpdateFdwHost || shouldUpdateFdwPort) { + const fwdOptionsQuery = `"foreignDataWrapperOptions" = jsonb_set(${ + shouldUpdateFdwDbname && shouldUpdateFdwHost && shouldUpdateFdwPort + ? `jsonb_set(jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{host}', to_jsonb($${parametersPositions['host']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)` + : shouldUpdateFdwDbname && shouldUpdateFdwHost + ? `jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{host}', to_jsonb($${parametersPositions['host']}::text)` + : shouldUpdateFdwDbname && shouldUpdateFdwPort + ? `jsonb_set("foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)` + : shouldUpdateFdwHost && shouldUpdateFdwPort + ? `jsonb_set("foreignDataWrapperOptions", '{host}', to_jsonb($${parametersPositions['host']}::text)), '{port}', to_jsonb($${parametersPositions['port']}::text)` + : shouldUpdateFdwDbname + ? `"foreignDataWrapperOptions", '{dbname}', to_jsonb($${parametersPositions['dbname']}::text)` + : shouldUpdateFdwHost + ? `"foreignDataWrapperOptions", '{host}', to_jsonb($${parametersPositions['host']}::text)` + : `"foreignDataWrapperOptions", '{port}', to_jsonb($${parametersPositions['port']}::text)` + })`; + + options.push(fwdOptionsQuery); + } + + const rawQuery = `UPDATE metadata."remoteServer" SET ${options.join( + ', ', + )} WHERE "id"= $1 RETURNING *`; + + return [parameters, rawQuery]; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/user-mapping-options-input.utils.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/user-mapping-options-input.utils.ts new file mode 100644 index 000000000..e6f245877 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/user-mapping-options-input.utils.ts @@ -0,0 +1,14 @@ +import { InputType, Field } from '@nestjs/graphql'; + +import { IsOptional } from 'class-validator'; + +@InputType() +export class UserMappingOptionsInput { + @IsOptional() + @Field(() => String, { nullable: true }) + username: string; + + @IsOptional() + @Field(() => String, { nullable: true }) + password: string; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts deleted file mode 100644 index e9cc3e542..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.ts +++ /dev/null @@ -1,20 +0,0 @@ -const INPUT_REGEX = /^([A-Za-z0-9\-_.@]+)$/; - -export const validateObject = (input: object) => { - for (const [key, value] of Object.entries(input)) { - // Password are encrypted so we don't need to validate them - if (key === 'password') { - continue; - } - - if (!INPUT_REGEX.test(value.toString())) { - throw new Error('Invalid remote server input'); - } - } -}; - -export const validateString = (input: string) => { - if (!INPUT_REGEX.test(input)) { - throw new Error('Invalid remote server input'); - } -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.utils.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.utils.ts new file mode 100644 index 000000000..99de7fa95 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/utils/validate-remote-server-input.utils.ts @@ -0,0 +1,26 @@ +import { BadRequestException } from '@nestjs/common'; + +import { isDefined } from 'class-validator'; + +const INPUT_REGEX = /^([A-Za-z0-9\-_.@]+)$/; + +export const validateObjectAgainstInjections = (input: object) => { + for (const [key, value] of Object.entries(input)) { + // Password are encrypted so we don't need to validate them + if (key === 'password') { + continue; + } + + if (!isDefined(value)) { + continue; + } + + validateStringAgainstInjections(value.toString()); + } +}; + +export const validateStringAgainstInjections = (input: string) => { + if (!INPUT_REGEX.test(input)) { + throw new BadRequestException('Invalid remote server input'); + } +};