fix(db): add more options for SSL connection, add envvar for base64 db cert (#1533)

This commit is contained in:
Waleed
2025-10-02 15:53:45 -07:00
committed by GitHub
parent 4bc37db547
commit fa9c97816b
12 changed files with 414 additions and 59 deletions

View File

@@ -1,6 +1,8 @@
# Database (Required)
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres"
# DATABASE_SSL=TRUE # Optional: Enable SSL for database connections (defaults to FALSE)
# DATABASE_SSL=disable # Optional: SSL mode (disable, prefer, require, verify-ca, verify-full)
# DATABASE_SSL_CA= # Optional: Base64-encoded CA certificate (required for verify-ca/verify-full)
# To generate: cat your-ca.crt | base64 | tr -d '\n'
# PostgreSQL Port (Optional) - defaults to 5432 if not specified
# POSTGRES_PORT=5432

View File

@@ -94,8 +94,6 @@ export async function GET(request: NextRequest) {
workflowUpdatedAt: workflow.updatedAt,
}
// Optimized query: Start by filtering workflows in the workspace with user permissions
// This ensures we scan only relevant logs instead of the entire table
const baseQuery = db
.select(selectColumns)
.from(workflowExecutionLogs)
@@ -103,7 +101,7 @@ export async function GET(request: NextRequest) {
workflow,
and(
eq(workflowExecutionLogs.workflowId, workflow.id),
eq(workflow.workspaceId, params.workspaceId) // Filter workspace during join!
eq(workflow.workspaceId, params.workspaceId)
)
)
.innerJoin(
@@ -184,7 +182,7 @@ export async function GET(request: NextRequest) {
.limit(params.limit)
.offset(params.offset)
// Get total count for pagination using the same optimized join structure
// Get total count for pagination using the same join structure
const countQuery = db
.select({ count: sql<number>`count(*)` })
.from(workflowExecutionLogs)
@@ -192,7 +190,7 @@ export async function GET(request: NextRequest) {
workflow,
and(
eq(workflowExecutionLogs.workflowId, workflow.id),
eq(workflow.workspaceId, params.workspaceId) // Same optimization
eq(workflow.workspaceId, params.workspaceId)
)
)
.innerJoin(

View File

@@ -106,7 +106,7 @@ export async function GET(request: NextRequest) {
const conditions = buildLogFilters(filters)
const orderBy = getOrderBy(params.order)
// Build and execute query - optimized to filter workspace during join
// Build and execute query
const baseQuery = db
.select({
id: workflowExecutionLogs.id,
@@ -128,7 +128,7 @@ export async function GET(request: NextRequest) {
workflow,
and(
eq(workflowExecutionLogs.workflowId, workflow.id),
eq(workflow.workspaceId, params.workspaceId) // Filter workspace during join!
eq(workflow.workspaceId, params.workspaceId)
)
)
.innerJoin(

View File

@@ -18,6 +18,8 @@ import { getEnv, isTruthy } from '@/lib/env'
import { isHosted } from '@/lib/environment'
import { cn } from '@/lib/utils'
import { useOrganizationStore } from '@/stores/organization'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useSubscriptionStore } from '@/stores/subscription/store'
const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED'))
@@ -200,6 +202,21 @@ export function SettingsNavigation({
{navigationItems.map((item) => (
<div key={item.id} className='mb-1'>
<button
onMouseEnter={() => {
switch (item.id) {
case 'general':
useGeneralStore.getState().loadSettings()
break
case 'subscription':
useSubscriptionStore.getState().loadData()
break
case 'team':
useOrganizationStore.getState().loadData()
break
default:
break
}
}}
onClick={() => onSectionChange(item.id)}
className={cn(
'group flex h-9 w-full cursor-pointer items-center rounded-[8px] px-2 py-2 font-medium font-sans text-sm transition-colors',

View File

@@ -21,6 +21,7 @@ import {
getVisiblePlans,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription-permissions'
import { useOrganizationStore } from '@/stores/organization'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useSubscriptionStore } from '@/stores/subscription/store'
const CONSTANTS = {
@@ -531,32 +532,14 @@ export function Subscription({ onOpenChange }: SubscriptionProps) {
}
function BillingUsageNotificationsToggle() {
const [enabled, setEnabled] = useState<boolean | null>(null)
const isLoading = useGeneralStore((s) => s.isBillingUsageNotificationsLoading)
const enabled = useGeneralStore((s) => s.isBillingUsageNotificationsEnabled)
const setEnabled = useGeneralStore((s) => s.setBillingUsageNotificationsEnabled)
const loadSettings = useGeneralStore((s) => s.loadSettings)
useEffect(() => {
let isMounted = true
const load = async () => {
const res = await fetch('/api/users/me/settings')
const json = await res.json()
const current = json?.data?.billingUsageNotificationsEnabled
if (isMounted) setEnabled(current !== false)
}
load()
return () => {
isMounted = false
}
}, [])
const update = async (next: boolean) => {
setEnabled(next)
await fetch('/api/users/me/settings', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ billingUsageNotificationsEnabled: next }),
})
}
if (enabled === null) return null
void loadSettings()
}, [loadSettings])
return (
<div className='mt-4 flex items-center justify-between'>
@@ -564,7 +547,13 @@ function BillingUsageNotificationsToggle() {
<span className='font-medium text-sm'>Usage notifications</span>
<span className='text-muted-foreground text-xs'>Email me when I reach 80% usage</span>
</div>
<Switch checked={enabled} onCheckedChange={(v: boolean) => update(v)} />
<Switch
checked={!!enabled}
disabled={isLoading}
onCheckedChange={(v: boolean) => {
void setEnabled(v)
}}
/>
</div>
)
}

View File

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

View File

@@ -1,17 +1,44 @@
import type { ConnectionOptions } from 'node:tls'
import * as schema from '@sim/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
import { and, eq, or, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { env, isTruthy } from '@/lib/env'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
const logger = createLogger('SocketDatabase')
const connectionString = env.DATABASE_URL
const useSSL = env.DATABASE_SSL === undefined ? false : isTruthy(env.DATABASE_SSL)
const getSSLConfig = () => {
const sslMode = env.DATABASE_SSL
if (!sslMode) return undefined
if (sslMode === 'disable') return false
if (sslMode === 'prefer') return 'prefer'
const sslConfig: ConnectionOptions = {}
if (sslMode === 'require') {
sslConfig.rejectUnauthorized = false
} else if (sslMode === 'verify-ca' || sslMode === 'verify-full') {
sslConfig.rejectUnauthorized = true
if (env.DATABASE_SSL_CA) {
try {
const ca = Buffer.from(env.DATABASE_SSL_CA, 'base64').toString('utf-8')
sslConfig.ca = ca
} catch (error) {
logger.error('Failed to parse DATABASE_SSL_CA:', error)
}
}
}
return sslConfig
}
const sslConfig = getSSLConfig()
const socketDb = drizzle(
postgres(connectionString, {
prepare: false,
@@ -20,7 +47,7 @@ const socketDb = drizzle(
max: 25,
onnotice: () => {},
debug: false,
ssl: useSSL ? 'require' : false,
...(sslConfig !== undefined && { ssl: sslConfig }),
}),
{ schema }
)
@@ -169,7 +196,7 @@ export async function persistWorkflowOperation(workflowId: string, operation: an
const { operation: op, target, payload, timestamp, userId } = operation
await db.transaction(async (tx) => {
// Handle different operation types within the transaction first
// Handle different operation types within the transaction
switch (target) {
case 'block':
await handleBlockOperationTx(tx, workflowId, op, payload, userId)

View File

@@ -1,15 +1,42 @@
import type { ConnectionOptions } from 'node:tls'
import * as schema from '@sim/db/schema'
import { workflowBlocks, workflowEdges } from '@sim/db/schema'
import { and, eq, isNull } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import type { Server } from 'socket.io'
import { env, isTruthy } from '@/lib/env'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
const connectionString = env.DATABASE_URL
const useSSL = env.DATABASE_SSL === undefined ? false : isTruthy(env.DATABASE_SSL)
const getSSLConfig = () => {
const sslMode = env.DATABASE_SSL
if (!sslMode) return undefined
if (sslMode === 'disable') return false
if (sslMode === 'prefer') return 'prefer'
const sslConfig: ConnectionOptions = {}
if (sslMode === 'require') {
sslConfig.rejectUnauthorized = false
} else if (sslMode === 'verify-ca' || sslMode === 'verify-full') {
sslConfig.rejectUnauthorized = true
if (env.DATABASE_SSL_CA) {
try {
const ca = Buffer.from(env.DATABASE_SSL_CA, 'base64').toString('utf-8')
sslConfig.ca = ca
} catch (error) {
console.error('Failed to parse DATABASE_SSL_CA:', error)
}
}
}
return sslConfig
}
const sslConfig = getSSLConfig()
const db = drizzle(
postgres(connectionString, {
prepare: false,
@@ -17,7 +44,7 @@ const db = drizzle(
connect_timeout: 20,
max: 5,
onnotice: () => {},
ssl: useSSL ? 'require' : false,
...(sslConfig !== undefined && { ssl: sslConfig }),
}),
{ schema }
)