Skip to content

Commit 991f044

Browse files
authored
v0.3.36: workflow block logs, whitelabeling configurability, session provider
2 parents e107363 + 2ebfb57 commit 991f044

File tree

61 files changed

+8222
-1070
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+8222
-1070
lines changed

apps/sim/app/api/auth/socket-token/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { auth } from '@/lib/auth'
44

55
export async function POST() {
66
try {
7+
const hdrs = await headers()
78
const response = await auth.api.generateOneTimeToken({
8-
headers: await headers(),
9+
headers: hdrs,
910
})
1011

1112
if (!response) {
@@ -14,7 +15,6 @@ export async function POST() {
1415

1516
return NextResponse.json({ token: response.token })
1617
} catch (error) {
17-
console.error('Error generating one-time token:', error)
1818
return NextResponse.json({ error: 'Failed to generate token' }, { status: 500 })
1919
}
2020
}

apps/sim/app/api/templates/[id]/route.ts

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { eq, sql } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
3+
import { z } from 'zod'
34
import { getSession } from '@/lib/auth'
45
import { createLogger } from '@/lib/logs/console/logger'
6+
import { hasAdminPermission } from '@/lib/permissions/utils'
57
import { db } from '@/db'
6-
import { templates } from '@/db/schema'
8+
import { templates, workflow } from '@/db/schema'
79

810
const logger = createLogger('TemplateByIdAPI')
911

@@ -62,3 +64,153 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
6264
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
6365
}
6466
}
67+
68+
const updateTemplateSchema = z.object({
69+
name: z.string().min(1).max(100),
70+
description: z.string().min(1).max(500),
71+
author: z.string().min(1).max(100),
72+
category: z.string().min(1),
73+
icon: z.string().min(1),
74+
color: z.string().regex(/^#[0-9A-F]{6}$/i),
75+
state: z.any().optional(), // Workflow state
76+
})
77+
78+
// PUT /api/templates/[id] - Update a template
79+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
80+
const requestId = crypto.randomUUID().slice(0, 8)
81+
const { id } = await params
82+
83+
try {
84+
const session = await getSession()
85+
if (!session?.user?.id) {
86+
logger.warn(`[${requestId}] Unauthorized template update attempt for ID: ${id}`)
87+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
88+
}
89+
90+
const body = await request.json()
91+
const validationResult = updateTemplateSchema.safeParse(body)
92+
93+
if (!validationResult.success) {
94+
logger.warn(`[${requestId}] Invalid template data for update: ${id}`, validationResult.error)
95+
return NextResponse.json(
96+
{ error: 'Invalid template data', details: validationResult.error.errors },
97+
{ status: 400 }
98+
)
99+
}
100+
101+
const { name, description, author, category, icon, color, state } = validationResult.data
102+
103+
// Check if template exists
104+
const existingTemplate = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
105+
106+
if (existingTemplate.length === 0) {
107+
logger.warn(`[${requestId}] Template not found for update: ${id}`)
108+
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
109+
}
110+
111+
// Permission: template owner OR admin of the workflow's workspace (if any)
112+
let canUpdate = existingTemplate[0].userId === session.user.id
113+
114+
if (!canUpdate && existingTemplate[0].workflowId) {
115+
const wfRows = await db
116+
.select({ workspaceId: workflow.workspaceId })
117+
.from(workflow)
118+
.where(eq(workflow.id, existingTemplate[0].workflowId))
119+
.limit(1)
120+
121+
const workspaceId = wfRows[0]?.workspaceId as string | null | undefined
122+
if (workspaceId) {
123+
const hasAdmin = await hasAdminPermission(session.user.id, workspaceId)
124+
if (hasAdmin) canUpdate = true
125+
}
126+
}
127+
128+
if (!canUpdate) {
129+
logger.warn(`[${requestId}] User denied permission to update template ${id}`)
130+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
131+
}
132+
133+
// Update the template
134+
const updatedTemplate = await db
135+
.update(templates)
136+
.set({
137+
name,
138+
description,
139+
author,
140+
category,
141+
icon,
142+
color,
143+
...(state && { state }),
144+
updatedAt: new Date(),
145+
})
146+
.where(eq(templates.id, id))
147+
.returning()
148+
149+
logger.info(`[${requestId}] Successfully updated template: ${id}`)
150+
151+
return NextResponse.json({
152+
data: updatedTemplate[0],
153+
message: 'Template updated successfully',
154+
})
155+
} catch (error: any) {
156+
logger.error(`[${requestId}] Error updating template: ${id}`, error)
157+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
158+
}
159+
}
160+
161+
// DELETE /api/templates/[id] - Delete a template
162+
export async function DELETE(
163+
request: NextRequest,
164+
{ params }: { params: Promise<{ id: string }> }
165+
) {
166+
const requestId = crypto.randomUUID().slice(0, 8)
167+
const { id } = await params
168+
169+
try {
170+
const session = await getSession()
171+
if (!session?.user?.id) {
172+
logger.warn(`[${requestId}] Unauthorized template delete attempt for ID: ${id}`)
173+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
174+
}
175+
176+
// Fetch template
177+
const existing = await db.select().from(templates).where(eq(templates.id, id)).limit(1)
178+
if (existing.length === 0) {
179+
logger.warn(`[${requestId}] Template not found for delete: ${id}`)
180+
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
181+
}
182+
183+
const template = existing[0]
184+
185+
// Permission: owner or admin of the workflow's workspace (if any)
186+
let canDelete = template.userId === session.user.id
187+
188+
if (!canDelete && template.workflowId) {
189+
// Look up workflow to get workspaceId
190+
const wfRows = await db
191+
.select({ workspaceId: workflow.workspaceId })
192+
.from(workflow)
193+
.where(eq(workflow.id, template.workflowId))
194+
.limit(1)
195+
196+
const workspaceId = wfRows[0]?.workspaceId as string | null | undefined
197+
if (workspaceId) {
198+
const hasAdmin = await hasAdminPermission(session.user.id, workspaceId)
199+
if (hasAdmin) canDelete = true
200+
}
201+
}
202+
203+
if (!canDelete) {
204+
logger.warn(`[${requestId}] User denied permission to delete template ${id}`)
205+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
206+
}
207+
208+
await db.delete(templates).where(eq(templates.id, id))
209+
210+
logger.info(`[${requestId}] Deleted template: ${id}`)
211+
return NextResponse.json({ success: true })
212+
} catch (error: any) {
213+
logger.error(`[${requestId}] Error deleting template: ${id}`, error)
214+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
215+
}
216+
}

apps/sim/app/api/templates/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const QueryParamsSchema = z.object({
7777
limit: z.coerce.number().optional().default(50),
7878
offset: z.coerce.number().optional().default(0),
7979
search: z.string().optional(),
80+
workflowId: z.string().optional(),
8081
})
8182

8283
// GET /api/templates - Retrieve templates
@@ -111,6 +112,11 @@ export async function GET(request: NextRequest) {
111112
)
112113
}
113114

115+
// Apply workflow filter if provided (for getting template by workflow)
116+
if (params.workflowId) {
117+
conditions.push(eq(templates.workflowId, params.workflowId))
118+
}
119+
114120
// Combine conditions
115121
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
116122

apps/sim/app/api/webhooks/[id]/route.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { eq } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { getSession } from '@/lib/auth'
4+
import { env } from '@/lib/env'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { getUserEntityPermissions } from '@/lib/permissions/utils'
7+
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
68
import { db } from '@/db'
79
import { webhook, workflow } from '@/db/schema'
810

@@ -242,6 +244,167 @@ export async function DELETE(
242244

243245
const foundWebhook = webhookData.webhook
244246

247+
// If it's an Airtable webhook, delete it from Airtable first
248+
if (foundWebhook.provider === 'airtable') {
249+
try {
250+
const { baseId, externalId } = (foundWebhook.providerConfig || {}) as {
251+
baseId?: string
252+
externalId?: string
253+
}
254+
255+
if (!baseId) {
256+
logger.warn(`[${requestId}] Missing baseId for Airtable webhook deletion.`, {
257+
webhookId: id,
258+
})
259+
return NextResponse.json(
260+
{ error: 'Missing baseId for Airtable webhook deletion' },
261+
{ status: 400 }
262+
)
263+
}
264+
265+
// Get access token for the workflow owner
266+
const userIdForToken = webhookData.workflow.userId
267+
const accessToken = await getOAuthToken(userIdForToken, 'airtable')
268+
if (!accessToken) {
269+
logger.warn(
270+
`[${requestId}] Could not retrieve Airtable access token for user ${userIdForToken}. Cannot delete webhook in Airtable.`,
271+
{ webhookId: id }
272+
)
273+
return NextResponse.json(
274+
{ error: 'Airtable access token not found for webhook deletion' },
275+
{ status: 401 }
276+
)
277+
}
278+
279+
// Resolve externalId if missing by listing webhooks and matching our notificationUrl
280+
let resolvedExternalId: string | undefined = externalId
281+
282+
if (!resolvedExternalId) {
283+
try {
284+
const requestOrigin = new URL(request.url).origin
285+
const effectiveOrigin = requestOrigin.includes('localhost')
286+
? env.NEXT_PUBLIC_APP_URL || requestOrigin
287+
: requestOrigin
288+
const expectedNotificationUrl = `${effectiveOrigin}/api/webhooks/trigger/${foundWebhook.path}`
289+
290+
const listUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks`
291+
const listResp = await fetch(listUrl, {
292+
headers: {
293+
Authorization: `Bearer ${accessToken}`,
294+
},
295+
})
296+
const listBody = await listResp.json().catch(() => null)
297+
298+
if (listResp.ok && listBody && Array.isArray(listBody.webhooks)) {
299+
const match = listBody.webhooks.find((w: any) => {
300+
const url: string | undefined = w?.notificationUrl
301+
if (!url) return false
302+
// Prefer exact match; fallback to suffix match to handle origin/host remaps
303+
return (
304+
url === expectedNotificationUrl ||
305+
url.endsWith(`/api/webhooks/trigger/${foundWebhook.path}`)
306+
)
307+
})
308+
if (match?.id) {
309+
resolvedExternalId = match.id as string
310+
// Persist resolved externalId for future operations
311+
try {
312+
await db
313+
.update(webhook)
314+
.set({
315+
providerConfig: {
316+
...(foundWebhook.providerConfig || {}),
317+
externalId: resolvedExternalId,
318+
},
319+
updatedAt: new Date(),
320+
})
321+
.where(eq(webhook.id, id))
322+
} catch {
323+
// non-fatal persistence error
324+
}
325+
logger.info(`[${requestId}] Resolved Airtable externalId by listing webhooks`, {
326+
baseId,
327+
externalId: resolvedExternalId,
328+
})
329+
} else {
330+
logger.warn(`[${requestId}] Could not resolve Airtable externalId from list`, {
331+
baseId,
332+
expectedNotificationUrl,
333+
})
334+
}
335+
} else {
336+
logger.warn(`[${requestId}] Failed to list Airtable webhooks to resolve externalId`, {
337+
baseId,
338+
status: listResp.status,
339+
body: listBody,
340+
})
341+
}
342+
} catch (e: any) {
343+
logger.warn(`[${requestId}] Error attempting to resolve Airtable externalId`, {
344+
error: e?.message,
345+
})
346+
}
347+
}
348+
349+
// If still not resolvable, skip remote deletion but proceed with local delete
350+
if (!resolvedExternalId) {
351+
logger.info(
352+
`[${requestId}] Airtable externalId not found; skipping remote deletion and proceeding to remove local record`,
353+
{ baseId }
354+
)
355+
}
356+
357+
if (resolvedExternalId) {
358+
const airtableDeleteUrl = `https://api.airtable.com/v0/bases/${baseId}/webhooks/${resolvedExternalId}`
359+
const airtableResponse = await fetch(airtableDeleteUrl, {
360+
method: 'DELETE',
361+
headers: {
362+
Authorization: `Bearer ${accessToken}`,
363+
},
364+
})
365+
366+
// Attempt to parse error body for better diagnostics
367+
if (!airtableResponse.ok) {
368+
let responseBody: any = null
369+
try {
370+
responseBody = await airtableResponse.json()
371+
} catch {
372+
// ignore parse errors
373+
}
374+
375+
logger.error(
376+
`[${requestId}] Failed to delete Airtable webhook in Airtable. Status: ${airtableResponse.status}`,
377+
{ baseId, externalId: resolvedExternalId, response: responseBody }
378+
)
379+
return NextResponse.json(
380+
{
381+
error: 'Failed to delete webhook from Airtable',
382+
details:
383+
(responseBody && (responseBody.error?.message || responseBody.error)) ||
384+
`Status ${airtableResponse.status}`,
385+
},
386+
{ status: 500 }
387+
)
388+
}
389+
390+
logger.info(`[${requestId}] Successfully deleted Airtable webhook in Airtable`, {
391+
baseId,
392+
externalId: resolvedExternalId,
393+
})
394+
}
395+
} catch (error: any) {
396+
logger.error(`[${requestId}] Error deleting Airtable webhook`, {
397+
webhookId: id,
398+
error: error.message,
399+
stack: error.stack,
400+
})
401+
return NextResponse.json(
402+
{ error: 'Failed to delete webhook from Airtable', details: error.message },
403+
{ status: 500 }
404+
)
405+
}
406+
}
407+
245408
// If it's a Telegram webhook, delete it from Telegram first
246409
if (foundWebhook.provider === 'telegram') {
247410
try {

0 commit comments

Comments
 (0)