Skip to content

Commit fa9c978

Browse files
authored
fix(db): add more options for SSL connection, add envvar for base64 db cert (#1533)
1 parent 4bc37db commit fa9c978

File tree

12 files changed

+414
-59
lines changed

12 files changed

+414
-59
lines changed

apps/sim/.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Database (Required)
22
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres"
3-
# DATABASE_SSL=TRUE # Optional: Enable SSL for database connections (defaults to FALSE)
3+
# DATABASE_SSL=disable # Optional: SSL mode (disable, prefer, require, verify-ca, verify-full)
4+
# DATABASE_SSL_CA= # Optional: Base64-encoded CA certificate (required for verify-ca/verify-full)
5+
# To generate: cat your-ca.crt | base64 | tr -d '\n'
46

57
# PostgreSQL Port (Optional) - defaults to 5432 if not specified
68
# POSTGRES_PORT=5432

apps/sim/app/api/logs/route.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,14 @@ export async function GET(request: NextRequest) {
9494
workflowUpdatedAt: workflow.updatedAt,
9595
}
9696

97-
// Optimized query: Start by filtering workflows in the workspace with user permissions
98-
// This ensures we scan only relevant logs instead of the entire table
9997
const baseQuery = db
10098
.select(selectColumns)
10199
.from(workflowExecutionLogs)
102100
.innerJoin(
103101
workflow,
104102
and(
105103
eq(workflowExecutionLogs.workflowId, workflow.id),
106-
eq(workflow.workspaceId, params.workspaceId) // Filter workspace during join!
104+
eq(workflow.workspaceId, params.workspaceId)
107105
)
108106
)
109107
.innerJoin(
@@ -184,15 +182,15 @@ export async function GET(request: NextRequest) {
184182
.limit(params.limit)
185183
.offset(params.offset)
186184

187-
// Get total count for pagination using the same optimized join structure
185+
// Get total count for pagination using the same join structure
188186
const countQuery = db
189187
.select({ count: sql<number>`count(*)` })
190188
.from(workflowExecutionLogs)
191189
.innerJoin(
192190
workflow,
193191
and(
194192
eq(workflowExecutionLogs.workflowId, workflow.id),
195-
eq(workflow.workspaceId, params.workspaceId) // Same optimization
193+
eq(workflow.workspaceId, params.workspaceId)
196194
)
197195
)
198196
.innerJoin(

apps/sim/app/api/v1/logs/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export async function GET(request: NextRequest) {
106106
const conditions = buildLogFilters(filters)
107107
const orderBy = getOrderBy(params.order)
108108

109-
// Build and execute query - optimized to filter workspace during join
109+
// Build and execute query
110110
const baseQuery = db
111111
.select({
112112
id: workflowExecutionLogs.id,
@@ -128,7 +128,7 @@ export async function GET(request: NextRequest) {
128128
workflow,
129129
and(
130130
eq(workflowExecutionLogs.workflowId, workflow.id),
131-
eq(workflow.workspaceId, params.workspaceId) // Filter workspace during join!
131+
eq(workflow.workspaceId, params.workspaceId)
132132
)
133133
)
134134
.innerJoin(

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { getEnv, isTruthy } from '@/lib/env'
1818
import { isHosted } from '@/lib/environment'
1919
import { cn } from '@/lib/utils'
2020
import { useOrganizationStore } from '@/stores/organization'
21+
import { useGeneralStore } from '@/stores/settings/general/store'
22+
import { useSubscriptionStore } from '@/stores/subscription/store'
2123

2224
const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
2325

@@ -200,6 +202,21 @@ export function SettingsNavigation({
200202
{navigationItems.map((item) => (
201203
<div key={item.id} className='mb-1'>
202204
<button
205+
onMouseEnter={() => {
206+
switch (item.id) {
207+
case 'general':
208+
useGeneralStore.getState().loadSettings()
209+
break
210+
case 'subscription':
211+
useSubscriptionStore.getState().loadData()
212+
break
213+
case 'team':
214+
useOrganizationStore.getState().loadData()
215+
break
216+
default:
217+
break
218+
}
219+
}}
203220
onClick={() => onSectionChange(item.id)}
204221
className={cn(
205222
'group flex h-9 w-full cursor-pointer items-center rounded-[8px] px-2 py-2 font-medium font-sans text-sm transition-colors',

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
getVisiblePlans,
2222
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription-permissions'
2323
import { useOrganizationStore } from '@/stores/organization'
24+
import { useGeneralStore } from '@/stores/settings/general/store'
2425
import { useSubscriptionStore } from '@/stores/subscription/store'
2526

2627
const CONSTANTS = {
@@ -531,40 +532,28 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
531532
}
532533

533534
function BillingUsageNotificationsToggle() {
534-
const [enabled, setEnabled] = useState<boolean | null>(null)
535+
const isLoading = useGeneralStore((s) => s.isBillingUsageNotificationsLoading)
536+
const enabled = useGeneralStore((s) => s.isBillingUsageNotificationsEnabled)
537+
const setEnabled = useGeneralStore((s) => s.setBillingUsageNotificationsEnabled)
538+
const loadSettings = useGeneralStore((s) => s.loadSettings)
535539

536540
useEffect(() => {
537-
let isMounted = true
538-
const load = async () => {
539-
const res = await fetch('/api/users/me/settings')
540-
const json = await res.json()
541-
const current = json?.data?.billingUsageNotificationsEnabled
542-
if (isMounted) setEnabled(current !== false)
543-
}
544-
load()
545-
return () => {
546-
isMounted = false
547-
}
548-
}, [])
549-
550-
const update = async (next: boolean) => {
551-
setEnabled(next)
552-
await fetch('/api/users/me/settings', {
553-
method: 'PATCH',
554-
headers: { 'Content-Type': 'application/json' },
555-
body: JSON.stringify({ billingUsageNotificationsEnabled: next }),
556-
})
557-
}
558-
559-
if (enabled === null) return null
541+
void loadSettings()
542+
}, [loadSettings])
560543

561544
return (
562545
<div className='mt-4 flex items-center justify-between'>
563546
<div className='flex flex-col'>
564547
<span className='font-medium text-sm'>Usage notifications</span>
565548
<span className='text-muted-foreground text-xs'>Email me when I reach 80% usage</span>
566549
</div>
567-
<Switch checked={enabled} onCheckedChange={(v: boolean) => update(v)} />
550+
<Switch
551+
checked={!!enabled}
552+
disabled={isLoading}
553+
onCheckedChange={(v: boolean) => {
554+
void setEnabled(v)
555+
}}
556+
/>
568557
</div>
569558
)
570559
}

apps/sim/lib/env.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export const env = createEnv({
1717
server: {
1818
// Core Database & Authentication
1919
DATABASE_URL: z.string().url(), // Primary database connection string
20-
DATABASE_SSL: z.boolean().optional(), // Enable SSL for database connections (defaults to false)
20+
DATABASE_SSL: z.enum(['disable', 'prefer', 'require', 'verify-ca', 'verify-full']).optional(), // PostgreSQL SSL mode
21+
DATABASE_SSL_CA: z.string().optional(), // Base64-encoded CA certificate for SSL verification
2122
BETTER_AUTH_URL: z.string().url(), // Base URL for Better Auth service
2223
BETTER_AUTH_SECRET: z.string().min(32), // Secret key for Better Auth JWT signing
2324
DISABLE_REGISTRATION: z.boolean().optional(), // Flag to disable new user registration

apps/sim/socket-server/database/operations.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,44 @@
1+
import type { ConnectionOptions } from 'node:tls'
12
import * as schema from '@sim/db'
23
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
34
import { and, eq, or, sql } from 'drizzle-orm'
45
import { drizzle } from 'drizzle-orm/postgres-js'
56
import postgres from 'postgres'
6-
import { env, isTruthy } from '@/lib/env'
7+
import { env } from '@/lib/env'
78
import { createLogger } from '@/lib/logs/console/logger'
89
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
910

1011
const logger = createLogger('SocketDatabase')
1112

1213
const connectionString = env.DATABASE_URL
13-
const useSSL = env.DATABASE_SSL === undefined ? false : isTruthy(env.DATABASE_SSL)
1414

15+
const getSSLConfig = () => {
16+
const sslMode = env.DATABASE_SSL
17+
18+
if (!sslMode) return undefined
19+
if (sslMode === 'disable') return false
20+
if (sslMode === 'prefer') return 'prefer'
21+
22+
const sslConfig: ConnectionOptions = {}
23+
24+
if (sslMode === 'require') {
25+
sslConfig.rejectUnauthorized = false
26+
} else if (sslMode === 'verify-ca' || sslMode === 'verify-full') {
27+
sslConfig.rejectUnauthorized = true
28+
if (env.DATABASE_SSL_CA) {
29+
try {
30+
const ca = Buffer.from(env.DATABASE_SSL_CA, 'base64').toString('utf-8')
31+
sslConfig.ca = ca
32+
} catch (error) {
33+
logger.error('Failed to parse DATABASE_SSL_CA:', error)
34+
}
35+
}
36+
}
37+
38+
return sslConfig
39+
}
40+
41+
const sslConfig = getSSLConfig()
1542
const socketDb = drizzle(
1643
postgres(connectionString, {
1744
prepare: false,
@@ -20,7 +47,7 @@ const socketDb = drizzle(
2047
max: 25,
2148
onnotice: () => {},
2249
debug: false,
23-
ssl: useSSL ? 'require' : false,
50+
...(sslConfig !== undefined && { ssl: sslConfig }),
2451
}),
2552
{ schema }
2653
)
@@ -169,7 +196,7 @@ export async function persistWorkflowOperation(workflowId: string, operation: an
169196
const { operation: op, target, payload, timestamp, userId } = operation
170197

171198
await db.transaction(async (tx) => {
172-
// Handle different operation types within the transaction first
199+
// Handle different operation types within the transaction
173200
switch (target) {
174201
case 'block':
175202
await handleBlockOperationTx(tx, workflowId, op, payload, userId)

apps/sim/socket-server/rooms/manager.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,50 @@
1+
import type { ConnectionOptions } from 'node:tls'
12
import * as schema from '@sim/db/schema'
23
import { workflowBlocks, workflowEdges } from '@sim/db/schema'
34
import { and, eq, isNull } from 'drizzle-orm'
45
import { drizzle } from 'drizzle-orm/postgres-js'
56
import postgres from 'postgres'
67
import type { Server } from 'socket.io'
7-
import { env, isTruthy } from '@/lib/env'
8+
import { env } from '@/lib/env'
89
import { createLogger } from '@/lib/logs/console/logger'
910

1011
const connectionString = env.DATABASE_URL
11-
const useSSL = env.DATABASE_SSL === undefined ? false : isTruthy(env.DATABASE_SSL)
1212

13+
const getSSLConfig = () => {
14+
const sslMode = env.DATABASE_SSL
15+
16+
if (!sslMode) return undefined
17+
if (sslMode === 'disable') return false
18+
if (sslMode === 'prefer') return 'prefer'
19+
20+
const sslConfig: ConnectionOptions = {}
21+
22+
if (sslMode === 'require') {
23+
sslConfig.rejectUnauthorized = false
24+
} else if (sslMode === 'verify-ca' || sslMode === 'verify-full') {
25+
sslConfig.rejectUnauthorized = true
26+
if (env.DATABASE_SSL_CA) {
27+
try {
28+
const ca = Buffer.from(env.DATABASE_SSL_CA, 'base64').toString('utf-8')
29+
sslConfig.ca = ca
30+
} catch (error) {
31+
console.error('Failed to parse DATABASE_SSL_CA:', error)
32+
}
33+
}
34+
}
35+
36+
return sslConfig
37+
}
38+
39+
const sslConfig = getSSLConfig()
1340
const db = drizzle(
1441
postgres(connectionString, {
1542
prepare: false,
1643
idle_timeout: 15,
1744
connect_timeout: 20,
1845
max: 5,
1946
onnotice: () => {},
20-
ssl: useSSL ? 'require' : false,
47+
...(sslConfig !== undefined && { ssl: sslConfig }),
2148
}),
2249
{ schema }
2350
)

packages/db/index.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ConnectionOptions } from 'node:tls'
12
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'
23
import postgres from 'postgres'
34
import * as schema from './schema'
@@ -10,20 +11,52 @@ if (!connectionString) {
1011
throw new Error('Missing DATABASE_URL environment variable')
1112
}
1213

13-
function isTruthy(value: string | undefined): boolean {
14-
if (!value) return false
15-
return value.toLowerCase() === 'true' || value === '1'
16-
}
14+
const getSSLConfig = () => {
15+
const sslMode = process.env.DATABASE_SSL?.toLowerCase()
16+
17+
if (!sslMode) {
18+
return undefined
19+
}
20+
21+
if (sslMode === 'disable') {
22+
return false
23+
}
24+
25+
if (sslMode === 'prefer') {
26+
return 'prefer'
27+
}
1728

18-
const useSSL = process.env.DATABASE_SSL === undefined ? false : isTruthy(process.env.DATABASE_SSL)
29+
const sslConfig: ConnectionOptions = {}
30+
31+
if (sslMode === 'require') {
32+
sslConfig.rejectUnauthorized = false
33+
} else if (sslMode === 'verify-ca' || sslMode === 'verify-full') {
34+
sslConfig.rejectUnauthorized = true
35+
if (process.env.DATABASE_SSL_CA) {
36+
try {
37+
const ca = Buffer.from(process.env.DATABASE_SSL_CA, 'base64').toString('utf-8')
38+
sslConfig.ca = ca
39+
} catch (error) {
40+
console.error('Failed to parse DATABASE_SSL_CA:', error)
41+
}
42+
}
43+
} else {
44+
throw new Error(
45+
`Invalid DATABASE_SSL mode: ${sslMode}. Must be one of: disable, prefer, require, verify-ca, verify-full`
46+
)
47+
}
48+
49+
return sslConfig
50+
}
1951

52+
const sslConfig = getSSLConfig()
2053
const postgresClient = postgres(connectionString, {
2154
prepare: false,
2255
idle_timeout: 20,
2356
connect_timeout: 30,
2457
max: 80,
2558
onnotice: () => {},
26-
ssl: useSSL ? 'require' : false,
59+
...(sslConfig !== undefined && { ssl: sslConfig }),
2760
})
2861

2962
const drizzleClient = drizzle(postgresClient, { schema })

0 commit comments

Comments
 (0)