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
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ docker compose -f docker-compose.prod.yml up -d

### Option 4: Manual Setup

**Requirements:**
- [Bun](https://bun.sh/) runtime
- PostgreSQL 12+ with [pgvector extension](https://github.com/pgvector/pgvector) (required for AI embeddings)

**Note:** Sim Studio uses vector embeddings for AI features like knowledge bases and semantic search, which requires the `pgvector` PostgreSQL extension.

1. Clone and install dependencies:

```bash
Expand All @@ -99,20 +105,43 @@ cd sim
bun install
```

2. Set up environment:
2. Set up PostgreSQL with pgvector:

You need PostgreSQL with the `vector` extension for embedding support. Choose one option:

**Option A: Using Docker (Recommended)**
```bash
# Start PostgreSQL with pgvector extension
docker run --name simstudio-db \
-e POSTGRES_PASSWORD=your_password \
-e POSTGRES_DB=simstudio \
-p 5432:5432 -d \
pgvector/pgvector:pg17
```

**Option B: Manual Installation**
- Install PostgreSQL 12+ and the pgvector extension
- See [pgvector installation guide](https://github.com/pgvector/pgvector#installation)

3. Set up environment:

```bash
cd apps/sim
cp .env.example .env # Configure with required variables (DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL)
```

3. Set up the database:
Update your `.env` file with the database URL:
```bash
DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
```

4. Set up the database:

```bash
bunx drizzle-kit push
bunx drizzle-kit migrate
```

4. Start the development servers:
5. Start the development servers:

**Recommended approach - run both servers together (from project root):**

Expand Down
25 changes: 14 additions & 11 deletions apps/sim/app/api/tools/confluence/pages/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console-logger'
import { getConfluenceCloudId } from '@/tools/confluence/utils'

const logger = createLogger('ConfluencePages')

export const dynamic = 'force-dynamic'

export async function POST(request: Request) {
Expand Down Expand Up @@ -39,7 +42,7 @@ export async function POST(request: Request) {
const queryString = queryParams.toString()
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl

console.log(`Fetching Confluence pages from: ${url}`)
logger.info(`Fetching Confluence pages from: ${url}`)

// Make the request to Confluence API with OAuth Bearer token
const response = await fetch(url, {
Expand All @@ -50,23 +53,23 @@ export async function POST(request: Request) {
},
})

console.log('Response status:', response.status, response.statusText)
logger.info('Response status:', response.status, response.statusText)

if (!response.ok) {
console.error(`Confluence API error: ${response.status} ${response.statusText}`)
logger.error(`Confluence API error: ${response.status} ${response.statusText}`)
let errorMessage

try {
const errorData = await response.json()
console.error('Error details:', JSON.stringify(errorData, null, 2))
logger.error('Error details:', JSON.stringify(errorData, null, 2))
errorMessage = errorData.message || `Failed to fetch Confluence pages (${response.status})`
} catch (e) {
console.error('Could not parse error response as JSON:', e)
logger.error('Could not parse error response as JSON:', e)

// Try to get the response text for more context
try {
const text = await response.text()
console.error('Response text:', text)
logger.error('Response text:', text)
errorMessage = `Failed to fetch Confluence pages: ${response.status} ${response.statusText}`
} catch (_textError) {
errorMessage = `Failed to fetch Confluence pages: ${response.status} ${response.statusText}`
Expand All @@ -77,13 +80,13 @@ export async function POST(request: Request) {
}

const data = await response.json()
console.log('Confluence API response:', `${JSON.stringify(data, null, 2).substring(0, 300)}...`)
console.log(`Found ${data.results?.length || 0} pages`)
logger.info('Confluence API response:', `${JSON.stringify(data, null, 2).substring(0, 300)}...`)
logger.info(`Found ${data.results?.length || 0} pages`)

if (data.results && data.results.length > 0) {
console.log('First few pages:')
logger.info('First few pages:')
for (const page of data.results.slice(0, 3)) {
console.log(`- ${page.id}: ${page.title}`)
logger.info(`- ${page.id}: ${page.title}`)
}
}

Expand All @@ -99,7 +102,7 @@ export async function POST(request: Request) {
})),
})
} catch (error) {
console.error('Error fetching Confluence pages:', error)
logger.error('Error fetching Confluence pages:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
Expand Down
52 changes: 52 additions & 0 deletions apps/sim/app/api/webhooks/test/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,58 @@ export async function GET(request: NextRequest) {
})
}

case 'microsoftteams': {
const hmacSecret = providerConfig.hmacSecret

if (!hmacSecret) {
logger.warn(`[${requestId}] Microsoft Teams webhook missing HMAC secret: ${webhookId}`)
return NextResponse.json(
{ success: false, error: 'Microsoft Teams webhook requires HMAC secret' },
{ status: 400 }
)
}

logger.info(`[${requestId}] Microsoft Teams webhook test successful: ${webhookId}`)
return NextResponse.json({
success: true,
webhook: {
id: foundWebhook.id,
url: webhookUrl,
isActive: foundWebhook.isActive,
},
message: 'Microsoft Teams outgoing webhook configuration is valid.',
setup: {
url: webhookUrl,
hmacSecretConfigured: !!hmacSecret,
instructions: [
'Create an outgoing webhook in Microsoft Teams',
'Set the callback URL to the webhook URL above',
'Copy the HMAC security token to the configuration',
'Users can trigger the webhook by @mentioning it in Teams',
],
},
test: {
curlCommand: `curl -X POST "${webhookUrl}" \\
-H "Content-Type: application/json" \\
-H "Authorization: HMAC <signature>" \\
-d '{"type":"message","text":"Hello from Microsoft Teams!","from":{"id":"test","name":"Test User"}}'`,
samplePayload: {
type: 'message',
id: '1234567890',
timestamp: new Date().toISOString(),
text: 'Hello Sim Studio Bot!',
from: {
id: '29:1234567890abcdef',
name: 'Test User',
},
conversation: {
id: '19:meeting_abcdef@thread.v2',
},
},
},
})
}

default: {
// Generic webhook test
logger.info(`[${requestId}] Generic webhook test successful: ${webhookId}`)
Expand Down
46 changes: 46 additions & 0 deletions apps/sim/app/api/webhooks/trigger/[path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
processGenericDeduplication,
processWebhook,
processWhatsAppDeduplication,
validateMicrosoftTeamsSignature,
} from '@/lib/webhooks/utils'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { db } from '@/db'
Expand Down Expand Up @@ -243,6 +244,51 @@ export async function POST(
return slackChallengeResponse
}

// Handle Microsoft Teams outgoing webhook signature verification (must be done before timeout)
if (foundWebhook.provider === 'microsoftteams') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}

if (providerConfig.hmacSecret) {
const authHeader = request.headers.get('authorization')

if (!authHeader || !authHeader.startsWith('HMAC ')) {
logger.warn(
`[${requestId}] Microsoft Teams outgoing webhook missing HMAC authorization header`
)
return new NextResponse('Unauthorized - Missing HMAC signature', { status: 401 })
}

// Get the raw body for HMAC verification
const rawBody = await request.text()
Comment on lines +261 to +262
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Request body is consumed twice - once here and previously in the initial parsing. This could cause issues.

Suggested change
// Get the raw body for HMAC verification
const rawBody = await request.text()
// Use the already parsed rawBody for HMAC verification
// const rawBody = await request.text() // This would fail since body already consumed


const isValidSignature = validateMicrosoftTeamsSignature(
providerConfig.hmacSecret,
authHeader,
rawBody
)

if (!isValidSignature) {
logger.warn(`[${requestId}] Microsoft Teams HMAC signature verification failed`)
return new NextResponse('Unauthorized - Invalid HMAC signature', { status: 401 })
}

logger.debug(`[${requestId}] Microsoft Teams HMAC signature verified successfully`)

// Parse the body again since we consumed it for verification
try {
body = JSON.parse(rawBody)
} catch (parseError) {
logger.error(
`[${requestId}] Failed to parse Microsoft Teams webhook body after verification`,
{
error: parseError instanceof Error ? parseError.message : String(parseError),
}
)
return new NextResponse('Invalid JSON payload', { status: 400 })
}
}
}

// Skip processing if another instance is already handling this request
if (!hasExecutionLock) {
logger.info(`[${requestId}] Skipping execution as lock was not acquired`)
Expand Down
13 changes: 8 additions & 5 deletions apps/sim/app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import crypto from 'crypto'
import { and, desc, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { permissions, workflow, workflowBlocks, workspace } from '@/db/schema'

const logger = createLogger('Workspaces')

// Get all workspaces for the current user
export async function GET() {
const session = await getSession()
Expand Down Expand Up @@ -244,12 +247,12 @@ async function createWorkspace(userId: string, name: string) {
updatedAt: now,
})

console.log(
`Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`
logger.info(
`Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}`
)
})
} catch (error) {
console.error(`Failed to create workspace ${workspaceId} with initial workflow:`, error)
logger.error(`Failed to create workspace ${workspaceId} with initial workflow:`, error)
throw error
}

Expand All @@ -276,7 +279,7 @@ async function migrateExistingWorkflows(userId: string, workspaceId: string) {
return // No orphaned workflows to migrate
}

console.log(
logger.info(
`Migrating ${orphanedWorkflows.length} workflows to workspace ${workspaceId} for user ${userId}`
)

Expand Down Expand Up @@ -308,6 +311,6 @@ async function ensureWorkflowsHaveWorkspace(userId: string, defaultWorkspaceId:
})
.where(and(eq(workflow.userId, userId), isNull(workflow.workspaceId)))

console.log(`Fixed ${orphanedWorkflows.length} orphaned workflows for user ${userId}`)
logger.info(`Fixed ${orphanedWorkflows.length} orphaned workflows for user ${userId}`)
}
}
11 changes: 7 additions & 4 deletions apps/sim/app/workspace/[workspaceId]/templates/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { and, eq } from 'drizzle-orm'
import { notFound } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { templateStars, templates } from '@/db/schema'
import type { Template } from '../templates'
import TemplateDetails from './template'

const logger = createLogger('TemplatePage')

interface TemplatePageProps {
params: Promise<{
workspaceId: string
Expand Down Expand Up @@ -58,7 +61,7 @@ export default async function TemplatePage({ params }: TemplatePageProps) {

// Validate that required fields are present
if (!template.id || !template.name || !template.author) {
console.error('Template missing required fields:', {
logger.error('Template missing required fields:', {
id: template.id,
name: template.name,
author: template.author,
Expand Down Expand Up @@ -100,9 +103,9 @@ export default async function TemplatePage({ params }: TemplatePageProps) {
isStarred,
}

console.log('Template from DB:', template)
console.log('Serialized template:', serializedTemplate)
console.log('Template state from DB:', template.state)
logger.info('Template from DB:', template)
logger.info('Serialized template:', serializedTemplate)
logger.info('Template state from DB:', template.state)

return (
<TemplateDetails
Expand Down
10 changes: 5 additions & 5 deletions apps/sim/app/workspace/[workspaceId]/templates/[id]/template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export default function TemplateDetails({
const renderWorkflowPreview = () => {
// Follow the same pattern as deployed-workflow-card.tsx
if (!template?.state) {
console.log('Template has no state:', template)
logger.info('Template has no state:', template)
return (
<div className='flex h-full items-center justify-center text-center'>
<div className='text-muted-foreground'>
Expand All @@ -154,10 +154,10 @@ export default function TemplateDetails({
)
}

console.log('Template state:', template.state)
console.log('Template state type:', typeof template.state)
console.log('Template state blocks:', template.state.blocks)
console.log('Template state edges:', template.state.edges)
logger.info('Template state:', template.state)
logger.info('Template state type:', typeof template.state)
logger.info('Template state blocks:', template.state.blocks)
logger.info('Template state edges:', template.state.edges)

try {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default function Templates({ initialTemplates, currentUserId }: Templates

const handleCreateNew = () => {
// TODO: Open create template modal or navigate to create page
console.log('Create new template')
logger.info('Create new template')
}

// Handle star change callback from template card
Expand Down
Loading