chore(server): drop unused postgresCredentials feature (#20573)
## Summary Drops the `postgresCredentials` legacy feature: a never-finished "postgres proxy" that would have let users query their workspace data over a standard Postgres connection. Nothing — frontend, e2e, Zapier, docs, other server code — calls these mutations/query. ## History - **Introduced** June 2024 (#5767, Thomas Trompette) as "first step for creating credentials for database proxy", alongside the Postgres FDW / remote-server work and the custom `twenty-postgres-spilo` image. Planned follow-ups (provisioning a DB on the proxy, mapping users, exposing it as a remote server) never landed. - **Abandoned** January 2026 (#17001, Weiko) when the sibling "remote integration" feature was removed as a BREAKING CHANGE — "not maintained for more than a year and never officially launched". The spilo image was then replaced with vanilla `postgres:16` (#19182, March 2026), retiring the FDW infrastructure entirely. - This PR finishes the cleanup: removes the orphaned module, the `allPostgresCredentials` relation, `JwtTokenTypeEnum.POSTGRES_PROXY` + payload, the reserved metadata keywords, and adds a 2.5.0 fast instance command that drops `core.postgresCredentials` (reversible `down`). Regenerated frontend GraphQL types + SDK metadata client. ## Test plan - [x] `tsgo --noEmit` clean on twenty-server + twenty-front; lint + prettier clean on touched files. - [x] `database:migrate:generate` reports no pending schema diff; server boots and serves the new schema.
This commit is contained in:
@@ -43,7 +43,6 @@ export enum JwtTokenTypeEnum {
|
||||
LOGIN = 'LOGIN',
|
||||
FILE = 'FILE',
|
||||
API_KEY = 'API_KEY',
|
||||
POSTGRES_PROXY = 'POSTGRES_PROXY',
|
||||
REMOTE_SERVER = 'REMOTE_SERVER',
|
||||
KEY_ENCRYPTION_KEY = 'KEY_ENCRYPTION_KEY',
|
||||
APPLICATION_ACCESS = 'APPLICATION_ACCESS',
|
||||
@@ -138,10 +137,6 @@ export type AccessTokenJwtPayload = CommonPropertiesJwtPayload & {
|
||||
impersonatedUserWorkspaceId?: string;
|
||||
};
|
||||
|
||||
export type PostgresProxyTokenJwtPayload = CommonPropertiesJwtPayload & {
|
||||
type: JwtTokenTypeEnum.POSTGRES_PROXY;
|
||||
};
|
||||
|
||||
export type AppOAuthStateJwtPayload = CommonPropertiesJwtPayload & {
|
||||
type: JwtTokenTypeEnum.APP_OAUTH_STATE;
|
||||
workspaceId: string;
|
||||
@@ -170,5 +165,4 @@ export type JwtPayload =
|
||||
| RefreshTokenJwtPayload
|
||||
| FileTokenJwtPayload
|
||||
| FileTokenJwtPayloadLegacy
|
||||
| PostgresProxyTokenJwtPayload
|
||||
| AppOAuthStateJwtPayload;
|
||||
|
||||
@@ -51,7 +51,6 @@ import { MessagingWebhooksModule } from 'src/engine/core-modules/messaging-webho
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module';
|
||||
import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module';
|
||||
import { PublicDomainModule } from 'src/engine/core-modules/public-domain/public-domain.module';
|
||||
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
|
||||
import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service';
|
||||
@@ -116,7 +115,6 @@ import { FileModule } from './file/file.module';
|
||||
PublicDomainModule,
|
||||
CloudflareModule,
|
||||
DnsManagerModule,
|
||||
PostgresCredentialsModule,
|
||||
WorkflowApiModule,
|
||||
WorkspaceEventEmitterModule,
|
||||
ActorModule,
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('PostgresCredentials')
|
||||
export class PostgresCredentialsDTO {
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field()
|
||||
user: string;
|
||||
|
||||
@Field()
|
||||
password: string;
|
||||
|
||||
@Field(() => UUIDScalarType)
|
||||
workspaceId: string;
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
@Entity({ name: 'postgresCredentials', schema: 'core' })
|
||||
@ObjectType('PostgresCredentials')
|
||||
export class PostgresCredentialsEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
user: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
passwordHash: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamptz' })
|
||||
deletedAt: Date;
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||
import { PostgresCredentialsEntity } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
||||
import { PostgresCredentialsResolver } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.resolver';
|
||||
import { PostgresCredentialsService } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.service';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule,
|
||||
TypeOrmModule.forFeature([PostgresCredentialsEntity]),
|
||||
PermissionsModule,
|
||||
],
|
||||
providers: [
|
||||
PostgresCredentialsResolver,
|
||||
PostgresCredentialsService,
|
||||
PostgresCredentialsEntity,
|
||||
],
|
||||
})
|
||||
export class PostgresCredentialsModule {}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { PostgresCredentialsDTO } from 'src/engine/core-modules/postgres-credentials/dtos/postgres-credentials.dto';
|
||||
import { PostgresCredentialsService } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.DATA_MODEL),
|
||||
)
|
||||
@MetadataResolver(() => PostgresCredentialsDTO)
|
||||
export class PostgresCredentialsResolver {
|
||||
constructor(
|
||||
private readonly postgresCredentialsService: PostgresCredentialsService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => PostgresCredentialsDTO)
|
||||
async enablePostgresProxy(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
return this.postgresCredentialsService.enablePostgresProxy(workspaceId);
|
||||
}
|
||||
|
||||
@Mutation(() => PostgresCredentialsDTO)
|
||||
async disablePostgresProxy(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
return this.postgresCredentialsService.disablePostgresProxy(workspaceId);
|
||||
}
|
||||
|
||||
@Query(() => PostgresCredentialsDTO, { nullable: true })
|
||||
async getPostgresCredentials(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
return this.postgresCredentialsService.getPostgresCredentials(workspaceId);
|
||||
}
|
||||
}
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
decryptText,
|
||||
encryptText,
|
||||
} from 'src/engine/core-modules/auth/auth.util';
|
||||
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { type PostgresCredentialsDTO } from 'src/engine/core-modules/postgres-credentials/dtos/postgres-credentials.dto';
|
||||
import { PostgresCredentialsEntity } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
||||
import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
export class PostgresCredentialsService {
|
||||
constructor(
|
||||
@InjectRepository(PostgresCredentialsEntity)
|
||||
private readonly postgresCredentialsRepository: Repository<PostgresCredentialsEntity>,
|
||||
private readonly jwtWrapperService: JwtWrapperService,
|
||||
) {}
|
||||
|
||||
async enablePostgresProxy(
|
||||
workspaceId: string,
|
||||
): Promise<PostgresCredentialsDTO> {
|
||||
const user = `user_${randomBytes(4).toString('hex')}`;
|
||||
const password = randomBytes(16).toString('hex');
|
||||
|
||||
const key = this.jwtWrapperService.generateAppSecret(
|
||||
JwtTokenTypeEnum.POSTGRES_PROXY,
|
||||
workspaceId,
|
||||
);
|
||||
const passwordHash = encryptText(password, key);
|
||||
|
||||
const existingCredentials =
|
||||
await this.postgresCredentialsRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCredentials) {
|
||||
throw new BadRequestException(
|
||||
'Postgres credentials already exist for this workspace',
|
||||
);
|
||||
}
|
||||
|
||||
const postgresCredentials = await this.postgresCredentialsRepository.create(
|
||||
{
|
||||
user,
|
||||
passwordHash,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
await this.postgresCredentialsRepository.save(postgresCredentials);
|
||||
|
||||
return {
|
||||
id: postgresCredentials.id,
|
||||
user,
|
||||
password,
|
||||
workspaceId,
|
||||
};
|
||||
}
|
||||
|
||||
async disablePostgresProxy(
|
||||
workspaceId: string,
|
||||
): Promise<PostgresCredentialsDTO> {
|
||||
const postgresCredentials =
|
||||
await this.postgresCredentialsRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!postgresCredentials?.id) {
|
||||
throw new NotFoundError(
|
||||
'No valid Postgres credentials not found for this workspace',
|
||||
);
|
||||
}
|
||||
|
||||
await this.postgresCredentialsRepository.delete({
|
||||
id: postgresCredentials.id,
|
||||
});
|
||||
|
||||
const key = this.jwtWrapperService.generateAppSecret(
|
||||
JwtTokenTypeEnum.POSTGRES_PROXY,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
id: postgresCredentials.id,
|
||||
user: postgresCredentials.user,
|
||||
password: decryptText(postgresCredentials.passwordHash, key),
|
||||
workspaceId: postgresCredentials.workspaceId,
|
||||
};
|
||||
}
|
||||
|
||||
async getPostgresCredentials(
|
||||
workspaceId: string,
|
||||
): Promise<PostgresCredentialsDTO | null> {
|
||||
const postgresCredentials =
|
||||
await this.postgresCredentialsRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!postgresCredentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = this.jwtWrapperService.generateAppSecret(
|
||||
JwtTokenTypeEnum.POSTGRES_PROXY,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
id: postgresCredentials.id,
|
||||
user: postgresCredentials.user,
|
||||
password: decryptText(postgresCredentials.passwordHash, key),
|
||||
workspaceId: postgresCredentials.workspaceId,
|
||||
};
|
||||
}
|
||||
}
|
||||
-1
@@ -10,7 +10,6 @@ export const WORKSPACE_ENTITY_NON_CACHED_PROPERTIES = [
|
||||
'emailingDomains',
|
||||
'publicDomains',
|
||||
'workspaceMembersCount',
|
||||
'allPostgresCredentials',
|
||||
'workspaceSSOIdentityProviders',
|
||||
'agents',
|
||||
'webhooks',
|
||||
|
||||
@@ -29,7 +29,6 @@ import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/em
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { KeyValuePairEntity } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { PostgresCredentialsEntity } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
||||
import { PublicDomainEntity } from 'src/engine/core-modules/public-domain/public-domain.entity';
|
||||
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
@@ -180,12 +179,6 @@ export class WorkspaceEntity {
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
suspendedAt: Date | null;
|
||||
|
||||
@OneToMany(
|
||||
() => PostgresCredentialsEntity,
|
||||
(postgresCredentials) => postgresCredentials.workspace,
|
||||
)
|
||||
allPostgresCredentials: Relation<PostgresCredentialsEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => WorkspaceSSOIdentityProviderEntity,
|
||||
(workspaceSSOIdentityProviders) => workspaceSSOIdentityProviders.workspace,
|
||||
|
||||
Reference in New Issue
Block a user