Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add endpoints to create and delete remote server #4606

Merged
merged 6 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/twenty-server/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ FRONT_BASE_URL=http://localhost:3001
ACCESS_TOKEN_SECRET=secret_jwt
LOGIN_TOKEN_SECRET=secret_login_tokens
REFRESH_TOKEN_SECRET=secret_refresh_token
FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh

# ———————— Optional ————————
# DEBUG_MODE=false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddRemoteServerTable1711374137222 implements MigrationInterface {
name = 'AddRemoteServerTable1711374137222';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "metadata"."remoteServer" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "foreignDataWrapperId" uuid NOT NULL DEFAULT uuid_generate_v4(), "foreignDataWrapperType" character varying, "foreignDataWrapperOptions" jsonb, "userMappingOptions" jsonb, "workspaceId" uuid NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8e5d208498fa2c9710bb934023a" PRIMARY KEY ("id"))`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "metadata"."remoteServer"`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';

import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { MetadataEngineModule } from 'src/engine/metadata-modules/metadata-engine.module';
import { CreateContextFactory } from 'src/engine/api/graphql/graphql-config/factories/create-context.factory';
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module';
import { metadataModuleFactory } from 'src/engine/api/graphql/metadata.module-factory';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
import { GraphQLConfigModule } from 'src/engine/api/graphql/graphql-config/graphql-config.module';
import { CreateContextFactory } from 'src/engine/api/graphql/graphql-config/factories/create-context.factory';
import { MetadataEngineModule } from 'src/engine/metadata-modules/metadata-engine.module';

@Module({
imports: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory';

import { ArgsAliasFactory } from './args-alias.factory';
import { ArgsStringFactory } from './args-string.factory';
import { RelationFieldAliasFactory } from './relation-field-alias.factory';
Expand Down Expand Up @@ -28,4 +30,5 @@ export const workspaceQueryBuilderFactories = [
UpdateOneQueryFactory,
UpdateManyQueryFactory,
DeleteManyQueryFactory,
ForeignDataWrapperQueryFactory,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';

import {
ForeignDataWrapperOptions,
RemoteServerType,
UserMappingOptions,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';

@Injectable()
export class ForeignDataWrapperQueryFactory {
createForeignDataWrapper(
foreignDataWrapperId: string,
foreignDataWrapperType: RemoteServerType,
foreignDataWrapperOptions: ForeignDataWrapperOptions<RemoteServerType>,
) {
const [name, options] = this.buildNameAndOptionsFromType(
foreignDataWrapperType,
foreignDataWrapperOptions,
);

return `CREATE SERVER "${foreignDataWrapperId}" FOREIGN DATA WRAPPER ${name} OPTIONS (${options})`;
}

createUserMapping(
foreignDataWrapperId: string,
userMappingOptions: UserMappingOptions,
) {
return `CREATE USER MAPPING IF NOT EXISTS FOR ${userMappingOptions.username} SERVER "${foreignDataWrapperId}" OPTIONS (user '${userMappingOptions.username}', password '${userMappingOptions.password}')`;
}

private buildNameAndOptionsFromType(
type: RemoteServerType,
options: ForeignDataWrapperOptions<RemoteServerType>,
) {
switch (type) {
case RemoteServerType.POSTGRES_FDW:
return ['postgres_fdw', this.buildPostgresFDWQueryOptions(options)];
default:
throw new Error('Foreign data wrapper type not supported');
}
}

private buildPostgresFDWQueryOptions(
foreignDataWrapperOptions: ForeignDataWrapperOptions<RemoteServerType>,
) {
return `dbname '${foreignDataWrapperOptions.dbname}', host '${foreignDataWrapperOptions.host}', port '${foreignDataWrapperOptions.port}'`;
}
}
41 changes: 41 additions & 0 deletions packages/twenty-server/src/engine/core-modules/auth/auth.util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createCipheriv, createDecipheriv, createHash } from 'crypto';

import * as bcrypt from 'bcrypt';

export const PASSWORD_REGEX = /^.{8,}$/;
Expand All @@ -13,3 +15,42 @@ export const hashPassword = async (password: string) => {
export const compareHash = async (password: string, passwordHash: string) => {
return bcrypt.compare(password, passwordHash);
};

export const encryptText = async (
textToEncrypt: string,
key: string,
iv: string,
): Promise<string> => {
const keyHash = createHash('sha512')
.update(key)
.digest('hex')
.substring(0, 32);

const ivHash = createHash('sha512').update(iv).digest('hex').substring(0, 16);

const cipher = createCipheriv('aes-256-ctr', keyHash, ivHash);

return Buffer.concat([cipher.update(textToEncrypt), cipher.final()]).toString(
'base64',
);
};

export const decryptText = async (
textToDecrypt: string,
key: string,
iv: string,
) => {
const keyHash = createHash('sha512')
.update(key)
.digest('hex')
.substring(0, 32);

const ivHash = createHash('sha512').update(iv).digest('hex').substring(0, 16);

const decipher = createDecipheriv('aes-256-ctr', keyHash, ivHash);

return Buffer.concat([
decipher.update(Buffer.from(textToDecrypt, 'base64')),
decipher.final(),
]).toString();
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timel
import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/timeline-calendar-event.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { HealthModule } from 'src/engine/core-modules/health/health.module';
import { RemoteServerModule } from 'src/engine/metadata-modules/remote-server/remote-server.module';

import { AnalyticsModule } from './analytics/analytics.module';
import { FileModule } from './file/file.module';
Expand All @@ -30,6 +31,7 @@ import { ClientConfigModule } from './client-config/client-config.module';
TimelineCalendarEventModule,
UserModule,
WorkspaceModule,
RemoteServerModule,
],
exports: [
AnalyticsModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { RelationMetadataModule } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.module';
import { RemoteServerModule } from 'src/engine/metadata-modules/remote-server/remote-server.module';
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';

Expand All @@ -15,13 +16,15 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
RelationMetadataModule,
WorkspaceCacheVersionModule,
WorkspaceMigrationModule,
RemoteServerModule,
],
providers: [],
exports: [
DataSourceModule,
FieldMetadataModule,
ObjectMetadataModule,
RelationMetadataModule,
RemoteServerModule,
],
})
export class MetadataEngineModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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';

@InputType()
export class CreateRemoteServerInput<T extends RemoteServerType> {
@Field(() => String)
foreignDataWrapperType: T;

@IsOptional()
@Field(() => GraphQLJSON)
foreignDataWrapperOptions: ForeignDataWrapperOptions<T>;

@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
userMappingOptions?: UserMappingOptions;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { InputType, ID } from '@nestjs/graphql';

import { IDField } from '@ptc-org/nestjs-query-graphql';

@InputType()
export class RemoteServerIdInput {
@IDField(() => ID, { description: 'The id of the record.' })
id!: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { InputType, Field } from '@nestjs/graphql';

import { IsString } from 'class-validator';

import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity';

@InputType()
export class RemoteServerTypeInput<T extends RemoteServerType> {
@Field(() => String)
@IsString()
foreignDataWrapperType!: T;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ObjectType, Field, HideField, ID } from '@nestjs/graphql';

import { IsOptional } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';

import {
ForeignDataWrapperOptions,
RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';

@ObjectType('RemoteServer')
export class RemoteServerDTO<T extends RemoteServerType> {
@Field(() => ID)
id: string;

@Field(() => ID)
foreignDataWrapperId: string;

@Field(() => String)
foreignDataWrapperType: T;

@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
foreignDataWrapperOptions?: ForeignDataWrapperOptions<T>;

@HideField()
workspaceId: string;

@Field()
createdAt: Date;

@Field()
updatedAt: Date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ObjectType } from '@nestjs/graphql';

import {
Column,
CreateDateColumn,
Entity,
Generated,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

export enum RemoteServerType {
POSTGRES_FDW = 'postgres_fdw',
}

type PostgresForeignDataWrapperOptions = {
host: string;
port: number;
dbname: string;
};

thomtrp marked this conversation as resolved.
Show resolved Hide resolved
export type ForeignDataWrapperOptions<T extends RemoteServerType> =
T extends RemoteServerType.POSTGRES_FDW
? PostgresForeignDataWrapperOptions
: never;

export type UserMappingOptions = {
username: string;
password: string;
};

@Entity('remoteServer')
@ObjectType('RemoteServer')
export class RemoteServerEntity<T extends RemoteServerType> {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
@Generated('uuid')
foreignDataWrapperId: string;

@Column({ nullable: true })
foreignDataWrapperType: T;

@Column({ nullable: true, type: 'jsonb' })
foreignDataWrapperOptions: ForeignDataWrapperOptions<T>;

@Column({ nullable: true, type: 'jsonb' })
userMappingOptions: UserMappingOptions;

@Column({ nullable: false, type: 'uuid' })
workspaceId: string;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { ForeignDataWrapperQueryFactory } from 'src/engine/api/graphql/workspace-query-builder/factories/foreign-data-wrapper-query.factory';
import { RemoteServerEntity } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { RemoteServerResolver } from 'src/engine/metadata-modules/remote-server/remote-server.resolver';
import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service';

@Module({
imports: [
TypeORMModule,
TypeOrmModule.forFeature([RemoteServerEntity], 'metadata'),
],
providers: [
RemoteServerService,
RemoteServerResolver,
ForeignDataWrapperQueryFactory,
],
exports: [RemoteServerService],
})
export class RemoteServerModule {}
Loading
Loading