From 7cdc4a0987e450fa20356241b18341c34b37edc9 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Oct 2025 15:33:51 -0700 Subject: [PATCH] fix(db): add more options for SSL connection, add envvar for base64 db cert --- apps/sim/.env.example | 4 +- apps/sim/app/api/logs/route.ts | 8 +- apps/sim/app/api/v1/logs/route.ts | 4 +- .../settings-navigation.tsx | 17 ++ .../components/subscription/subscription.tsx | 39 ++-- apps/sim/lib/env.ts | 3 +- apps/sim/socket-server/database/operations.ts | 35 ++- apps/sim/socket-server/rooms/manager.ts | 33 ++- packages/db/index.ts | 45 +++- .../db/scripts/deregister-sso-provider.ts | 211 ++++++++++++++++++ .../db/scripts/migrate-deployment-versions.ts | 37 ++- packages/db/scripts/register-sso-provider.ts | 37 ++- 12 files changed, 414 insertions(+), 59 deletions(-) create mode 100644 packages/db/scripts/deregister-sso-provider.ts diff --git a/apps/sim/.env.example b/apps/sim/.env.example index a533cd0a8e..b7808de726 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -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 diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index cc7cc4a0e9..e6e9505826 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -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`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( diff --git a/apps/sim/app/api/v1/logs/route.ts b/apps/sim/app/api/v1/logs/route.ts index 0ad3c6a890..fb6170a171 100644 --- a/apps/sim/app/api/v1/logs/route.ts +++ b/apps/sim/app/api/v1/logs/route.ts @@ -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( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx index 749458520a..6626e34fd4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/settings-navigation/settings-navigation.tsx @@ -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) => (
) } diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index e03cac4313..e3b5c05b1d 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -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 diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts index f807e870eb..35a73cbf9a 100644 --- a/apps/sim/socket-server/database/operations.ts +++ b/apps/sim/socket-server/database/operations.ts @@ -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) diff --git a/apps/sim/socket-server/rooms/manager.ts b/apps/sim/socket-server/rooms/manager.ts index 1c8d9630d4..3338c6b217 100644 --- a/apps/sim/socket-server/rooms/manager.ts +++ b/apps/sim/socket-server/rooms/manager.ts @@ -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 } ) diff --git a/packages/db/index.ts b/packages/db/index.ts index 5a441a8108..ab5a41a79f 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -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' @@ -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 }) diff --git a/packages/db/scripts/deregister-sso-provider.ts b/packages/db/scripts/deregister-sso-provider.ts new file mode 100644 index 0000000000..8ba55a1ff3 --- /dev/null +++ b/packages/db/scripts/deregister-sso-provider.ts @@ -0,0 +1,211 @@ +#!/usr/bin/env bun + +/** + * Deregister SSO Provider Script + * + * This script removes an SSO provider from the database for a specific user. + * + * Usage: bun run packages/db/scripts/deregister-sso-provider.ts + * + * Required Environment Variables: + * DATABASE_URL=your-database-url + * SSO_USER_EMAIL=user@domain.com (user whose SSO provider to remove) + * SSO_PROVIDER_ID=provider-id (optional, if not provided will remove all providers for user) + */ + +import type { ConnectionOptions } from 'node:tls' +import { and, eq } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/postgres-js' +import postgres from 'postgres' +import { ssoProvider, user } from '../schema' + +// Simple console logger +const logger = { + info: (message: string, meta?: any) => { + const timestamp = new Date().toISOString() + console.log( + `[${timestamp}] [INFO] [DeregisterSSODB] ${message}`, + meta ? JSON.stringify(meta, null, 2) : '' + ) + }, + error: (message: string, meta?: any) => { + const timestamp = new Date().toISOString() + console.error( + `[${timestamp}] [ERROR] [DeregisterSSODB] ${message}`, + meta ? JSON.stringify(meta, null, 2) : '' + ) + }, + warn: (message: string, meta?: any) => { + const timestamp = new Date().toISOString() + console.warn( + `[${timestamp}] [WARN] [DeregisterSSODB] ${message}`, + meta ? JSON.stringify(meta, null, 2) : '' + ) + }, +} + +// Get database URL from environment +const CONNECTION_STRING = process.env.DATABASE_URL +if (!CONNECTION_STRING) { + console.error('❌ DATABASE_URL environment variable is required') + process.exit(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 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(CONNECTION_STRING, { + prepare: false, + idle_timeout: 20, + connect_timeout: 30, + max: 10, + onnotice: () => {}, + ...(sslConfig !== undefined && { ssl: sslConfig }), +}) +const db = drizzle(postgresClient) + +async function getUser(email: string): Promise<{ id: string; email: string } | null> { + try { + const users = await db.select().from(user).where(eq(user.email, email)) + if (users.length === 0) { + logger.error(`No user found with email: ${email}`) + return null + } + return { id: users[0].id, email: users[0].email } + } catch (error) { + logger.error('Failed to query user:', error) + return null + } +} + +async function deregisterSSOProvider(): Promise { + try { + const userEmail = process.env.SSO_USER_EMAIL + if (!userEmail) { + logger.error('❌ SSO_USER_EMAIL environment variable is required') + logger.error('') + logger.error('Example usage:') + logger.error( + ' SSO_USER_EMAIL=admin@company.com bun run packages/db/scripts/deregister-sso-provider.ts' + ) + logger.error('') + logger.error('Optional: SSO_PROVIDER_ID=provider-id (to remove specific provider)') + return false + } + + // Get user + const targetUser = await getUser(userEmail) + if (!targetUser) { + return false + } + + logger.info(`Found user: ${targetUser.email} (ID: ${targetUser.id})`) + + // Get SSO providers for this user + const providers = await db + .select() + .from(ssoProvider) + .where(eq(ssoProvider.userId, targetUser.id)) + + if (providers.length === 0) { + logger.warn(`No SSO providers found for user: ${targetUser.email}`) + return false + } + + logger.info(`Found ${providers.length} SSO provider(s) for user ${targetUser.email}`) + for (const provider of providers) { + logger.info(` - Provider ID: ${provider.providerId}, Domain: ${provider.domain}`) + } + + // Check if specific provider ID was requested + const specificProviderId = process.env.SSO_PROVIDER_ID + + if (specificProviderId) { + // Delete specific provider + const providerToDelete = providers.find((p) => p.providerId === specificProviderId) + if (!providerToDelete) { + logger.error(`Provider '${specificProviderId}' not found for user ${targetUser.email}`) + return false + } + + await db + .delete(ssoProvider) + .where( + and(eq(ssoProvider.userId, targetUser.id), eq(ssoProvider.providerId, specificProviderId)) + ) + + logger.info( + `✅ Successfully deleted SSO provider '${specificProviderId}' for user ${targetUser.email}` + ) + } else { + // Delete all providers for this user + await db.delete(ssoProvider).where(eq(ssoProvider.userId, targetUser.id)) + + logger.info( + `✅ Successfully deleted all ${providers.length} SSO provider(s) for user ${targetUser.email}` + ) + } + + return true + } catch (error) { + logger.error('❌ Failed to deregister SSO provider:', { + error: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + }) + return false + } finally { + try { + await postgresClient.end({ timeout: 5 }) + } catch {} + } +} + +async function main() { + console.log('🗑️ Deregister SSO Provider Script') + console.log('====================================') + console.log('This script removes SSO provider records from the database.\n') + + const success = await deregisterSSOProvider() + + if (success) { + console.log('\n🎉 SSO provider deregistration completed successfully!') + process.exit(0) + } else { + console.log('\n💥 SSO deregistration failed. Check the logs above for details.') + process.exit(1) + } +} + +// Handle script execution +main().catch((error) => { + logger.error('Script execution failed:', { error }) + process.exit(1) +}) diff --git a/packages/db/scripts/migrate-deployment-versions.ts b/packages/db/scripts/migrate-deployment-versions.ts index 54a15345a5..a9b736e8fe 100644 --- a/packages/db/scripts/migrate-deployment-versions.ts +++ b/packages/db/scripts/migrate-deployment-versions.ts @@ -3,6 +3,7 @@ // This script is intentionally self-contained for execution in the migrations image. // Do not import from the main app code; duplicate minimal schema and DB setup here. +import type { ConnectionOptions } from 'node:tls' import { sql } from 'drizzle-orm' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' @@ -117,20 +118,44 @@ const workflowDeploymentVersion = pgTable( ) // ---------- DB client ---------- -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 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` + ) + } -const useSSL = process.env.DATABASE_SSL === undefined ? false : isTruthy(process.env.DATABASE_SSL) + return sslConfig +} +const sslConfig = getSSLConfig() const postgresClient = postgres(CONNECTION_STRING, { prepare: false, idle_timeout: 20, connect_timeout: 30, max: 10, onnotice: () => {}, - ssl: useSSL ? 'require' : false, + ...(sslConfig !== undefined && { ssl: sslConfig }), }) const db = drizzle(postgresClient) diff --git a/packages/db/scripts/register-sso-provider.ts b/packages/db/scripts/register-sso-provider.ts index 1eb585439c..ca252f4985 100644 --- a/packages/db/scripts/register-sso-provider.ts +++ b/packages/db/scripts/register-sso-provider.ts @@ -32,6 +32,7 @@ * SSO_SAML_WANT_ASSERTIONS_SIGNED=true (optional, defaults to false) */ +import type { ConnectionOptions } from 'node:tls' import { eq } from 'drizzle-orm' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' @@ -140,20 +141,44 @@ if (!CONNECTION_STRING) { process.exit(1) } -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 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` + ) + } -const useSSL = process.env.DATABASE_SSL === undefined ? false : isTruthy(process.env.DATABASE_SSL) + return sslConfig +} +const sslConfig = getSSLConfig() const postgresClient = postgres(CONNECTION_STRING, { prepare: false, idle_timeout: 20, connect_timeout: 30, max: 10, onnotice: () => {}, - ssl: useSSL ? 'require' : false, + ...(sslConfig !== undefined && { ssl: sslConfig }), }) const db = drizzle(postgresClient)