Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/sim/.env.example
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 3 additions & 5 deletions apps/sim/app/api/logs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,14 @@ 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)
.innerJoin(
workflow,
and(
eq(workflowExecutionLogs.workflowId, workflow.id),
eq(workflow.workspaceId, params.workspaceId) // Filter workspace during join!
eq(workflow.workspaceId, params.workspaceId)
)
)
.innerJoin(
Expand Down Expand Up @@ -184,15 +182,15 @@ 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)
.innerJoin(
workflow,
and(
eq(workflowExecutionLogs.workflowId, workflow.id),
eq(workflow.workspaceId, params.workspaceId) // Same optimization
eq(workflow.workspaceId, params.workspaceId)
)
)
.innerJoin(
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/v1/logs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'))

Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -531,40 +532,28 @@ 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'>
<div className='flex flex-col'>
<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>
)
}
3 changes: 2 additions & 1 deletion apps/sim/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 31 additions & 4 deletions apps/sim/socket-server/database/operations.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,7 +47,7 @@ const socketDb = drizzle(
max: 25,
onnotice: () => {},
debug: false,
ssl: useSSL ? 'require' : false,
...(sslConfig !== undefined && { ssl: sslConfig }),
}),
{ schema }
)
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 30 additions & 3 deletions apps/sim/socket-server/rooms/manager.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
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,
idle_timeout: 15,
connect_timeout: 20,
max: 5,
onnotice: () => {},
ssl: useSSL ? 'require' : false,
...(sslConfig !== undefined && { ssl: sslConfig }),
}),
{ schema }
)
Expand Down
45 changes: 39 additions & 6 deletions packages/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ConnectionOptions } from 'node:tls'
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
Expand All @@ -10,20 +11,52 @@ if (!connectionString) {
throw new Error('Missing DATABASE_URL environment variable')
}

function isTruthy(value: string | undefined): boolean {
if (!value) return false
return value.toLowerCase() === 'true' || value === '1'
}
const getSSLConfig = () => {
const sslMode = process.env.DATABASE_SSL?.toLowerCase()

if (!sslMode) {
return undefined
}

if (sslMode === 'disable') {
return false
}

if (sslMode === 'prefer') {
return 'prefer'
}

const useSSL = process.env.DATABASE_SSL === undefined ? false : isTruthy(process.env.DATABASE_SSL)
const sslConfig: ConnectionOptions = {}

if (sslMode === 'require') {
sslConfig.rejectUnauthorized = false
} else if (sslMode === 'verify-ca' || sslMode === 'verify-full') {
sslConfig.rejectUnauthorized = true
if (process.env.DATABASE_SSL_CA) {
try {
const ca = Buffer.from(process.env.DATABASE_SSL_CA, 'base64').toString('utf-8')
sslConfig.ca = ca
} catch (error) {
console.error('Failed to parse DATABASE_SSL_CA:', error)
}
}
} else {
throw new Error(
`Invalid DATABASE_SSL mode: ${sslMode}. Must be one of: disable, prefer, require, verify-ca, verify-full`
)
}

return sslConfig
}

const sslConfig = getSSLConfig()
const postgresClient = postgres(connectionString, {
prepare: false,
idle_timeout: 20,
connect_timeout: 30,
max: 80,
onnotice: () => {},
ssl: useSSL ? 'require' : false,
...(sslConfig !== undefined && { ssl: sslConfig }),
})

const drizzleClient = drizzle(postgresClient, { schema })
Expand Down
Loading