From 92e3f52b47edd3c52cea4ad74436b46d717c372a Mon Sep 17 00:00:00 2001 From: Mir Arif Hasan Date: Wed, 10 Dec 2025 12:19:17 +0600 Subject: [PATCH] chore: add sslmode support to PrismaService database URL parser (#5671) --- .../mock-server/mock-server.service.spec.ts | 35 ++++---- .../src/prisma/prisma.service.ts | 81 ++++++++++++++++--- 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts b/packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts index c7489382..964491f3 100644 --- a/packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts +++ b/packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts @@ -507,13 +507,9 @@ describe('MockServerService', () => { ); expect(E.isRight(result)).toBe(true); - expect(mockUserCollectionService.createUserCollection).toHaveBeenCalledWith( - user, - autoCreateInput.name, - null, - null, - 'REST', - ); + expect( + mockUserCollectionService.createUserCollection, + ).toHaveBeenCalledWith(user, autoCreateInput.name, null, null, 'REST'); expect(mockPrisma.mockServer.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -549,7 +545,9 @@ describe('MockServerService', () => { ); expect(E.isRight(result)).toBe(true); - expect(mockUserCollectionService.importCollectionsFromJSON).toHaveBeenCalled(); + expect( + mockUserCollectionService.importCollectionsFromJSON, + ).toHaveBeenCalled(); expect(mockPrisma.mockServer.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -629,7 +627,9 @@ describe('MockServerService', () => { ); expect(E.isRight(result)).toBe(true); - expect(mockTeamCollectionService.importCollectionsFromJSON).toHaveBeenCalled(); + expect( + mockTeamCollectionService.importCollectionsFromJSON, + ).toHaveBeenCalled(); expect(mockPrisma.mockServer.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ @@ -700,7 +700,10 @@ describe('MockServerService', () => { autoCreateRequestExample: false, }; - const createdCollection = { ...userCollection, id: 'rollback-coll-123' }; + const createdCollection = { + ...userCollection, + id: 'rollback-coll-123', + }; mockUserCollectionService.createUserCollection.mockResolvedValue( E.right(createdCollection as any), ); @@ -717,10 +720,9 @@ describe('MockServerService', () => { ); expect(E.isLeft(result)).toBe(true); - expect(mockUserCollectionService.deleteUserCollection).toHaveBeenCalledWith( - 'rollback-coll-123', - user.uid, - ); + expect( + mockUserCollectionService.deleteUserCollection, + ).toHaveBeenCalledWith('rollback-coll-123', user.uid); }); test('should rollback team collection on mock server creation failure', async () => { @@ -733,7 +735,10 @@ describe('MockServerService', () => { autoCreateRequestExample: false, }; - const createdTeamColl = { ...teamCollection, id: 'rollback-team-coll-123' }; + const createdTeamColl = { + ...teamCollection, + id: 'rollback-team-coll-123', + }; mockPrisma.team.findFirst.mockResolvedValue({ id: 'team123' } as any); mockTeamCollectionService.createCollection.mockResolvedValue( E.right(createdTeamColl as any), diff --git a/packages/hoppscotch-backend/src/prisma/prisma.service.ts b/packages/hoppscotch-backend/src/prisma/prisma.service.ts index c0a83b2f..58c50baf 100644 --- a/packages/hoppscotch-backend/src/prisma/prisma.service.ts +++ b/packages/hoppscotch-backend/src/prisma/prisma.service.ts @@ -13,57 +13,112 @@ export class PrismaService constructor() { const databaseUrl = process.env.DATABASE_URL; - if (!databaseUrl) { throw new Error('DATABASE_URL environment variable is not set'); } - const { connectionString, schema, connectionLimit, connectTimeout } = - PrismaService.parseDatabaseUrl(databaseUrl); + const parsed = PrismaService.parseDatabaseUrl(databaseUrl); + + // Generic SSL configuration for all database environments + // Supports: AWS Aurora, Docker, local PostgreSQL, managed databases + const sslConfig = PrismaService.getSSLConfig(parsed.sslMode); const pool = new pg.Pool({ - connectionString, - max: connectionLimit ?? 20, + connectionString: parsed.connectionString, + max: parsed.connectionLimit ?? 20, idleTimeoutMillis: 30000, - connectionTimeoutMillis: connectTimeout ?? 5000, + connectionTimeoutMillis: parsed.connectTimeout ?? 10000, + ssl: sslConfig, }); const adapter = new PrismaPg(pool, { - schema, + schema: parsed.schema, }); super({ adapter, transactionOptions: { - maxWait: 5000, // 5 seconds - timeout: 10000, // 10 seconds + maxWait: 5000, + timeout: 10000, }, }); this.pool = pool; } + /** + * --- SSL Configuration --- + * Generic SSL handling for various database environments + * - Local/Docker: No SSL (sslmode=disable or no sslmode) + * - AWS Aurora/RDS: SSL with relaxed validation (common for managed databases) + * - Custom: Set sslmode=verify-full for strict certificate validation + */ + private static getSSLConfig( + sslMode?: string, + ): false | { rejectUnauthorized: boolean } { + if (!sslMode || sslMode === 'disable') { + // Local PostgreSQL, Docker containers - no SSL + return false; + } + + if (sslMode === 'require' || sslMode === 'prefer' || sslMode === 'allow') { + // AWS Aurora, managed databases - SSL with relaxed validation + // This is a pragmatic approach for cloud databases where: + // - Connection is encrypted (prevents eavesdropping) + // - Network isolation (VPC/firewall) provides additional security + // - Certificate validation issues are common with managed services + return { rejectUnauthorized: false }; + } + + if (sslMode === 'verify-ca' || sslMode === 'verify-full') { + // Strict certificate validation - requires proper CA certificates + // Note: May require additional configuration for Prisma v7 + adapter-pg + return { rejectUnauthorized: true }; + } + + // Default to no SSL for unknown modes + return false; + } + + /** + * --- DATABASE_URL Parser --- + * Accepts: + * ?schema=custom + * ?connection_limit=10 + * ?connect_timeout=5000 + * ?sslmode=disable|prefer|require|verify-ca|verify-full + */ private static parseDatabaseUrl(databaseUrl: string): { connectionString: string; schema: string; connectionLimit?: number; connectTimeout?: number; + sslMode?: string; } { try { const url = new URL(databaseUrl); const schema = url.searchParams.get('schema') || 'public'; - const connectionLimit = url.searchParams.get('connection_limit'); - const connectTimeout = url.searchParams.get('connect_timeout'); + const connectionLimit = parseIntSafe( + url.searchParams.get('connection_limit'), + ); + const connectTimeout = parseIntSafe( + url.searchParams.get('connect_timeout'), + ); + const sslMode = url.searchParams.get('sslmode'); + // Remove all custom parameters including sslmode + // We handle SSL configuration programmatically via the ssl option url.searchParams.delete('schema'); url.searchParams.delete('connection_limit'); url.searchParams.delete('connect_timeout'); + url.searchParams.delete('sslmode'); return { connectionString: url.toString(), schema, - connectionLimit: parseIntSafe(connectionLimit), - connectTimeout: parseIntSafe(connectTimeout), + connectionLimit, + connectTimeout, + sslMode: sslMode || undefined, }; } catch (error) { throw new Error(