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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
# bun specific
bun-debug.log*

# this repo uses bun.lock; package-lock.json files are accidental
package-lock.json

# testing
/coverage
/apps/**/coverage
Expand Down
21 changes: 18 additions & 3 deletions apps/sim/app/api/auth/trello/authorize/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { authorizeTrelloContract } from '@/lib/api/contracts/oauth-connections'
import { parseRequest } from '@/lib/api/server'
Expand All @@ -12,6 +13,10 @@ const logger = createLogger('TrelloAuthorize')

export const dynamic = 'force-dynamic'

const TRELLO_STATE_COOKIE = 'trello_oauth_state'
const TRELLO_STATE_COOKIE_PATH = '/api/auth/trello'
const TRELLO_STATE_COOKIE_MAX_AGE_SECONDS = 60 * 10

export const GET = withRouteHandler(async (request: NextRequest) => {
try {
const session = await getSession()
Expand All @@ -30,7 +35,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
}

const baseUrl = getBaseUrl()
const returnUrl = `${baseUrl}/api/auth/trello/callback`
const state = generateShortId(32)
const returnUrl = new URL('/api/auth/trello/callback', baseUrl)
returnUrl.searchParams.set('state', state)
const scope = getCanonicalScopesForProvider('trello').join(',')

const authUrl = new URL('https://trello.com/1/authorize')
Expand All @@ -40,9 +47,17 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
authUrl.searchParams.set('callback_method', 'fragment')
authUrl.searchParams.set('response_type', 'token')
authUrl.searchParams.set('scope', scope)
authUrl.searchParams.set('return_url', returnUrl)
authUrl.searchParams.set('return_url', returnUrl.toString())

return NextResponse.redirect(authUrl.toString())
const response = NextResponse.redirect(authUrl.toString())
response.cookies.set(TRELLO_STATE_COOKIE, state, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: TRELLO_STATE_COOKIE_MAX_AGE_SECONDS,
path: TRELLO_STATE_COOKIE_PATH,
})
return response
} catch (error) {
logger.error('Error initiating Trello authorization:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
Expand Down
40 changes: 39 additions & 1 deletion apps/sim/app/api/auth/trello/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,54 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { trelloCallbackContract } from '@/lib/api/contracts/oauth-connections'
import { parseRequest } from '@/lib/api/server'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

const logger = createLogger('TrelloCallback')

export const dynamic = 'force-dynamic'

const TRELLO_STATE_COOKIE = 'trello_oauth_state'

function escapeForJsString(value: string): string {
return value.replace(/[\\'"<>&\r\n\u2028\u2029]/g, (ch) => {
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`
})
}

function renderErrorPage(baseUrl: string, redirectQuery: string) {
return new NextResponse(
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Trello connection failed</title></head><body><script>window.location.href=${JSON.stringify(`${baseUrl}/workspace?${redirectQuery}`)};</script><p>Trello connection failed. Redirecting...</p></body></html>`,
{
status: 400,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store, no-cache, must-revalidate',
},
}
)
}

export const GET = withRouteHandler(async (request: NextRequest) => {
const parsed = await parseRequest(trelloCallbackContract, request, {})
if (!parsed.success) return parsed.response

const baseUrl = getBaseUrl()
const queryState = parsed.data.query.state
const cookieState = request.cookies.get(TRELLO_STATE_COOKIE)?.value

if (!queryState || !cookieState || queryState !== cookieState) {
logger.warn('Trello callback rejected: state mismatch or missing state', {
hasQueryState: Boolean(queryState),
hasCookieState: Boolean(cookieState),
})
const response = renderErrorPage(baseUrl, 'error=trello_state_mismatch')
response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: '/api/auth/trello' })
return response
}

const safeState = escapeForJsString(queryState)

return new NextResponse(
`<!DOCTYPE html>
Expand Down Expand Up @@ -97,7 +135,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ token: token })
body: JSON.stringify({ token: token, state: '${safeState}' })
})
.then(response => response.json())
.then(data => {
Expand Down
26 changes: 24 additions & 2 deletions apps/sim/app/api/auth/trello/store/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ const logger = createLogger('TrelloStore')

export const dynamic = 'force-dynamic'

const TRELLO_STATE_COOKIE = 'trello_oauth_state'
const TRELLO_STATE_COOKIE_PATH = '/api/auth/trello'

function clearStateCookie(response: NextResponse) {
response.cookies.delete({ name: TRELLO_STATE_COOKIE, path: TRELLO_STATE_COOKIE_PATH })
return response
}

export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const session = await getSession()
Expand All @@ -26,7 +34,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

const parsed = await parseRequest(storeTrelloTokenContract, request, {})
if (!parsed.success) return parsed.response
const { token } = parsed.data.body
const { token, state } = parsed.data.body

const cookieState = request.cookies.get(TRELLO_STATE_COOKIE)?.value
if (!cookieState || cookieState !== state) {
logger.warn('Trello store rejected: state mismatch', {
hasCookieState: Boolean(cookieState),
userId: session.user.id,
})
return clearStateCookie(
NextResponse.json(
{ success: false, error: 'Invalid or expired authorization state' },
{ status: 400 }
)
)
}

const apiKey = env.TRELLO_API_KEY
if (!apiKey) {
Expand Down Expand Up @@ -123,7 +145,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}
}

return NextResponse.json({ success: true })
return clearStateCookie(NextResponse.json({ success: true }))
} catch (error) {
logger.error('Error storing Trello token:', error)
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 })
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/chat/manage/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export const PATCH = withRouteHandler(
outputConfigs,
} = validatedData

if (workflowId && workflowId !== existingChat[0].workflowId) {
return createErrorResponse('Changing the workflow of a chat deployment is not allowed', 400)
}

if (identifier && identifier !== existingChat[0].identifier) {
const existingIdentifier = await db
.select()
Expand Down Expand Up @@ -156,7 +160,6 @@ export const PATCH = withRouteHandler(
updatedAt: new Date(),
}

if (workflowId) updateData.workflowId = workflowId
if (identifier) updateData.identifier = identifier
if (title) updateData.title = title
if (description !== undefined) updateData.description = description
Expand Down
29 changes: 18 additions & 11 deletions apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { dataverseUploadFileContract } from '@/lib/api/contracts/tools/microsoft'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
Expand Down Expand Up @@ -78,20 +79,26 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const baseUrl = validatedData.environmentUrl.replace(/\/$/, '')
const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName}(${validatedData.recordId})/${validatedData.fileColumn}`

const response = await fetch(uploadUrl, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': 'application/octet-stream',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0',
'x-ms-file-name': validatedData.fileName,
const response = await secureFetchWithValidation(
uploadUrl,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'Content-Type': 'application/octet-stream',
'OData-MaxVersion': '4.0',
'OData-Version': '4.0',
'x-ms-file-name': validatedData.fileName,
},
body: fileBuffer,
},
body: new Uint8Array(fileBuffer),
})
'environmentUrl'
)

if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const errorData = (await response.json().catch(() => ({}))) as {
error?: { message?: string }
}
const errorMessage =
errorData?.error?.message ??
`Dataverse API error: ${response.status} ${response.statusText}`
Expand Down
24 changes: 13 additions & 11 deletions apps/sim/app/api/v1/logs/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { db } from '@sim/db'
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import { workflow, workflowExecutionLogs } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v1GetLogContract } from '@/lib/api/contracts/v1/logs'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta'
import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware'
import {
checkRateLimit,
createRateLimitResponse,
validateWorkspaceAccess,
} from '@/app/api/v1/middleware'

const logger = createLogger('V1LogDetailsAPI')

Expand Down Expand Up @@ -37,6 +41,7 @@ export const GET = withRouteHandler(
.select({
id: workflowExecutionLogs.id,
workflowId: workflowExecutionLogs.workflowId,
workspaceId: workflowExecutionLogs.workspaceId,
executionId: workflowExecutionLogs.executionId,
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
level: workflowExecutionLogs.level,
Expand All @@ -59,14 +64,6 @@ export const GET = withRouteHandler(
})
.from(workflowExecutionLogs)
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
.innerJoin(
permissions,
and(
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
eq(permissions.userId, userId)
)
)
.where(eq(workflowExecutionLogs.id, id))
.limit(1)

Expand All @@ -75,6 +72,11 @@ export const GET = withRouteHandler(
return NextResponse.json({ error: 'Log not found' }, { status: 404 })
}

const accessError = await validateWorkspaceAccess(rateLimit, userId, log.workspaceId)
if (accessError) {
return NextResponse.json({ error: 'Log not found' }, { status: 404 })
}

const workflowSummary = {
id: log.workflowId,
name: log.workflowName || 'Deleted Workflow',
Expand Down
29 changes: 14 additions & 15 deletions apps/sim/app/api/v1/logs/executions/[executionId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { db } from '@sim/db'
import { permissions, workflowExecutionLogs, workflowExecutionSnapshots } from '@sim/db/schema'
import { workflowExecutionLogs, workflowExecutionSnapshots } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v1GetExecutionContract } from '@/lib/api/contracts/v1/logs'
import { parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta'
import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware'
import {
checkRateLimit,
createRateLimitResponse,
validateWorkspaceAccess,
} from '@/app/api/v1/middleware'

const logger = createLogger('V1ExecutionAPI')

Expand All @@ -31,26 +35,21 @@ export const GET = withRouteHandler(
logger.debug(`Fetching execution data for: ${executionId}`)

const rows = await db
.select({
log: workflowExecutionLogs,
})
.select()
.from(workflowExecutionLogs)
.innerJoin(
permissions,
and(
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
eq(permissions.userId, userId)
)
)
.where(eq(workflowExecutionLogs.executionId, executionId))
.limit(1)

if (rows.length === 0) {
return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 })
}

const { log: workflowLog } = rows[0]
const workflowLog = rows[0]

const accessError = await validateWorkspaceAccess(rateLimit, userId, workflowLog.workspaceId)
if (accessError) {
return NextResponse.json({ error: 'Workflow execution not found' }, { status: 404 })
}

const [snapshot] = await db
.select()
Expand Down
9 changes: 8 additions & 1 deletion apps/sim/app/api/v1/logs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { buildLogFilters, getOrderBy } from '@/app/api/v1/logs/filters'
import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta'
import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware'
import {
checkRateLimit,
checkWorkspaceScope,
createRateLimitResponse,
} from '@/app/api/v1/middleware'

const logger = createLogger('V1LogsAPI')

Expand Down Expand Up @@ -62,6 +66,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {

const params = parsed.data.query

const scopeError = checkWorkspaceScope(rateLimit, params.workspaceId)
if (scopeError) return scopeError

logger.info(`[${requestId}] Fetching logs for workspace ${params.workspaceId}`, {
userId,
filters: {
Expand Down
Loading
Loading