Skip to content

Commit 92e3f52

Browse files
authored
chore: add sslmode support to PrismaService database URL parser (hoppscotch#5671)
1 parent d67a85b commit 92e3f52

File tree

2 files changed

+88
-28
lines changed

2 files changed

+88
-28
lines changed

packages/hoppscotch-backend/src/mock-server/mock-server.service.spec.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -507,13 +507,9 @@ describe('MockServerService', () => {
507507
);
508508

509509
expect(E.isRight(result)).toBe(true);
510-
expect(mockUserCollectionService.createUserCollection).toHaveBeenCalledWith(
511-
user,
512-
autoCreateInput.name,
513-
null,
514-
null,
515-
'REST',
516-
);
510+
expect(
511+
mockUserCollectionService.createUserCollection,
512+
).toHaveBeenCalledWith(user, autoCreateInput.name, null, null, 'REST');
517513
expect(mockPrisma.mockServer.create).toHaveBeenCalledWith(
518514
expect.objectContaining({
519515
data: expect.objectContaining({
@@ -549,7 +545,9 @@ describe('MockServerService', () => {
549545
);
550546

551547
expect(E.isRight(result)).toBe(true);
552-
expect(mockUserCollectionService.importCollectionsFromJSON).toHaveBeenCalled();
548+
expect(
549+
mockUserCollectionService.importCollectionsFromJSON,
550+
).toHaveBeenCalled();
553551
expect(mockPrisma.mockServer.create).toHaveBeenCalledWith(
554552
expect.objectContaining({
555553
data: expect.objectContaining({
@@ -629,7 +627,9 @@ describe('MockServerService', () => {
629627
);
630628

631629
expect(E.isRight(result)).toBe(true);
632-
expect(mockTeamCollectionService.importCollectionsFromJSON).toHaveBeenCalled();
630+
expect(
631+
mockTeamCollectionService.importCollectionsFromJSON,
632+
).toHaveBeenCalled();
633633
expect(mockPrisma.mockServer.create).toHaveBeenCalledWith(
634634
expect.objectContaining({
635635
data: expect.objectContaining({
@@ -700,7 +700,10 @@ describe('MockServerService', () => {
700700
autoCreateRequestExample: false,
701701
};
702702

703-
const createdCollection = { ...userCollection, id: 'rollback-coll-123' };
703+
const createdCollection = {
704+
...userCollection,
705+
id: 'rollback-coll-123',
706+
};
704707
mockUserCollectionService.createUserCollection.mockResolvedValue(
705708
E.right(createdCollection as any),
706709
);
@@ -717,10 +720,9 @@ describe('MockServerService', () => {
717720
);
718721

719722
expect(E.isLeft(result)).toBe(true);
720-
expect(mockUserCollectionService.deleteUserCollection).toHaveBeenCalledWith(
721-
'rollback-coll-123',
722-
user.uid,
723-
);
723+
expect(
724+
mockUserCollectionService.deleteUserCollection,
725+
).toHaveBeenCalledWith('rollback-coll-123', user.uid);
724726
});
725727

726728
test('should rollback team collection on mock server creation failure', async () => {
@@ -733,7 +735,10 @@ describe('MockServerService', () => {
733735
autoCreateRequestExample: false,
734736
};
735737

736-
const createdTeamColl = { ...teamCollection, id: 'rollback-team-coll-123' };
738+
const createdTeamColl = {
739+
...teamCollection,
740+
id: 'rollback-team-coll-123',
741+
};
737742
mockPrisma.team.findFirst.mockResolvedValue({ id: 'team123' } as any);
738743
mockTeamCollectionService.createCollection.mockResolvedValue(
739744
E.right(createdTeamColl as any),

packages/hoppscotch-backend/src/prisma/prisma.service.ts

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,57 +13,112 @@ export class PrismaService
1313

1414
constructor() {
1515
const databaseUrl = process.env.DATABASE_URL;
16-
1716
if (!databaseUrl) {
1817
throw new Error('DATABASE_URL environment variable is not set');
1918
}
2019

21-
const { connectionString, schema, connectionLimit, connectTimeout } =
22-
PrismaService.parseDatabaseUrl(databaseUrl);
20+
const parsed = PrismaService.parseDatabaseUrl(databaseUrl);
21+
22+
// Generic SSL configuration for all database environments
23+
// Supports: AWS Aurora, Docker, local PostgreSQL, managed databases
24+
const sslConfig = PrismaService.getSSLConfig(parsed.sslMode);
2325

2426
const pool = new pg.Pool({
25-
connectionString,
26-
max: connectionLimit ?? 20,
27+
connectionString: parsed.connectionString,
28+
max: parsed.connectionLimit ?? 20,
2729
idleTimeoutMillis: 30000,
28-
connectionTimeoutMillis: connectTimeout ?? 5000,
30+
connectionTimeoutMillis: parsed.connectTimeout ?? 10000,
31+
ssl: sslConfig,
2932
});
3033

3134
const adapter = new PrismaPg(pool, {
32-
schema,
35+
schema: parsed.schema,
3336
});
3437

3538
super({
3639
adapter,
3740
transactionOptions: {
38-
maxWait: 5000, // 5 seconds
39-
timeout: 10000, // 10 seconds
41+
maxWait: 5000,
42+
timeout: 10000,
4043
},
4144
});
4245

4346
this.pool = pool;
4447
}
4548

49+
/**
50+
* --- SSL Configuration ---
51+
* Generic SSL handling for various database environments
52+
* - Local/Docker: No SSL (sslmode=disable or no sslmode)
53+
* - AWS Aurora/RDS: SSL with relaxed validation (common for managed databases)
54+
* - Custom: Set sslmode=verify-full for strict certificate validation
55+
*/
56+
private static getSSLConfig(
57+
sslMode?: string,
58+
): false | { rejectUnauthorized: boolean } {
59+
if (!sslMode || sslMode === 'disable') {
60+
// Local PostgreSQL, Docker containers - no SSL
61+
return false;
62+
}
63+
64+
if (sslMode === 'require' || sslMode === 'prefer' || sslMode === 'allow') {
65+
// AWS Aurora, managed databases - SSL with relaxed validation
66+
// This is a pragmatic approach for cloud databases where:
67+
// - Connection is encrypted (prevents eavesdropping)
68+
// - Network isolation (VPC/firewall) provides additional security
69+
// - Certificate validation issues are common with managed services
70+
return { rejectUnauthorized: false };
71+
}
72+
73+
if (sslMode === 'verify-ca' || sslMode === 'verify-full') {
74+
// Strict certificate validation - requires proper CA certificates
75+
// Note: May require additional configuration for Prisma v7 + adapter-pg
76+
return { rejectUnauthorized: true };
77+
}
78+
79+
// Default to no SSL for unknown modes
80+
return false;
81+
}
82+
83+
/**
84+
* --- DATABASE_URL Parser ---
85+
* Accepts:
86+
* ?schema=custom
87+
* ?connection_limit=10
88+
* ?connect_timeout=5000
89+
* ?sslmode=disable|prefer|require|verify-ca|verify-full
90+
*/
4691
private static parseDatabaseUrl(databaseUrl: string): {
4792
connectionString: string;
4893
schema: string;
4994
connectionLimit?: number;
5095
connectTimeout?: number;
96+
sslMode?: string;
5197
} {
5298
try {
5399
const url = new URL(databaseUrl);
54100
const schema = url.searchParams.get('schema') || 'public';
55-
const connectionLimit = url.searchParams.get('connection_limit');
56-
const connectTimeout = url.searchParams.get('connect_timeout');
101+
const connectionLimit = parseIntSafe(
102+
url.searchParams.get('connection_limit'),
103+
);
104+
const connectTimeout = parseIntSafe(
105+
url.searchParams.get('connect_timeout'),
106+
);
107+
const sslMode = url.searchParams.get('sslmode');
57108

109+
// Remove all custom parameters including sslmode
110+
// We handle SSL configuration programmatically via the ssl option
58111
url.searchParams.delete('schema');
59112
url.searchParams.delete('connection_limit');
60113
url.searchParams.delete('connect_timeout');
114+
url.searchParams.delete('sslmode');
61115

62116
return {
63117
connectionString: url.toString(),
64118
schema,
65-
connectionLimit: parseIntSafe(connectionLimit),
66-
connectTimeout: parseIntSafe(connectTimeout),
119+
connectionLimit,
120+
connectTimeout,
121+
sslMode: sslMode || undefined,
67122
};
68123
} catch (error) {
69124
throw new Error(

0 commit comments

Comments
 (0)