-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Expand file tree
/
Copy pathroute.ts
More file actions
313 lines (274 loc) · 10.7 KB
/
route.ts
File metadata and controls
313 lines (274 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
import { db } from '@sim/db'
import {
templateCreators,
templateStars,
templates,
workflow,
workflowDeploymentVersion,
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, desc, eq, ilike, or, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
import {
extractRequiredCredentials,
sanitizeCredentials,
} from '@/lib/workflows/credentials/credential-extractor'
const logger = createLogger('TemplatesAPI')
export const revalidate = 0
// Function to sanitize sensitive data from workflow state
// Now uses the more comprehensive sanitizeCredentials from credential-extractor
function sanitizeWorkflowState(state: any): any {
return sanitizeCredentials(state)
}
// Schema for creating a template
const CreateTemplateSchema = z.object({
workflowId: z.string().min(1, 'Workflow ID is required'),
name: z.string().min(1, 'Name is required').max(100, 'Name must be less than 100 characters'),
details: z
.object({
tagline: z.string().max(500, 'Tagline must be less than 500 characters').optional(),
about: z.string().optional(), // Markdown long description
})
.optional(),
creatorId: z.string().min(1, 'Creator profile is required'),
tags: z.array(z.string()).max(10, 'Maximum 10 tags allowed').optional().default([]),
})
// Schema for query parameters
const QueryParamsSchema = z.object({
limit: z.coerce.number().optional().default(50),
offset: z.coerce.number().optional().default(0),
search: z.string().optional(),
workflowId: z.string().optional(),
status: z.enum(['pending', 'approved', 'rejected']).optional(),
includeAllStatuses: z.coerce.boolean().optional().default(false), // For super users
})
// GET /api/templates - Retrieve templates
export async function GET(request: NextRequest) {
const requestId = generateRequestId()
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized templates access attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const params = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
// Check if user is a super user
const { effectiveSuperUser } = await verifyEffectiveSuperUser(session.user.id)
const isSuperUser = effectiveSuperUser
// Build query conditions
const conditions = []
// Apply workflow filter if provided (for getting template by workflow)
// When fetching by workflowId, we want to get the template regardless of status
// This is used by the deploy modal to check if a template exists
if (params.workflowId) {
conditions.push(eq(templates.workflowId, params.workflowId))
// Don't apply status filter when fetching by workflowId - we want to show
// the template to its owner even if it's pending
} else {
// Apply status filter - only approved templates for non-super users
if (params.status) {
conditions.push(eq(templates.status, params.status))
} else if (!isSuperUser || !params.includeAllStatuses) {
// Non-super users and super users without includeAllStatuses flag see only approved templates
conditions.push(eq(templates.status, 'approved'))
}
}
// Apply search filter if provided
if (params.search) {
const searchTerm = `%${params.search}%`
conditions.push(
or(
ilike(templates.name, searchTerm),
sql`${templates.details}->>'tagline' ILIKE ${searchTerm}`
)
)
}
// Combine conditions
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined
// Apply ordering, limit, and offset with star information
const results = await db
.select({
id: templates.id,
workflowId: templates.workflowId,
name: templates.name,
details: templates.details,
creatorId: templates.creatorId,
creator: templateCreators,
views: templates.views,
stars: templates.stars,
status: templates.status,
tags: templates.tags,
requiredCredentials: templates.requiredCredentials,
state: templates.state,
createdAt: templates.createdAt,
updatedAt: templates.updatedAt,
isStarred: sql<boolean>`CASE WHEN ${templateStars.id} IS NOT NULL THEN true ELSE false END`,
isSuperUser: sql<boolean>`${isSuperUser}`, // Include super user status in response
})
.from(templates)
.leftJoin(
templateStars,
and(eq(templateStars.templateId, templates.id), eq(templateStars.userId, session.user.id))
)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(whereCondition)
.orderBy(desc(templates.views), desc(templates.createdAt))
.limit(params.limit)
.offset(params.offset)
// Get total count for pagination
const totalCount = await db
.select({ count: sql<number>`count(*)` })
.from(templates)
.where(whereCondition)
const total = totalCount[0]?.count || 0
logger.info(`[${requestId}] Successfully retrieved ${results.length} templates`)
return NextResponse.json({
data: results,
pagination: {
total,
limit: params.limit,
offset: params.offset,
page: Math.floor(params.offset / params.limit) + 1,
totalPages: Math.ceil(total / params.limit),
},
})
} catch (error: any) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid query parameters`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid query parameters', details: error.errors },
{ status: 400 }
)
}
logger.error(`[${requestId}] Error fetching templates`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
// POST /api/templates - Create a new template
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized template creation attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const data = CreateTemplateSchema.parse(body)
// Verify the workflow exists and belongs to the user
const workflowExists = await db
.select({ id: workflow.id })
.from(workflow)
.where(eq(workflow.id, data.workflowId))
.limit(1)
if (workflowExists.length === 0) {
logger.warn(`[${requestId}] Workflow not found: ${data.workflowId}`)
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
}
const { verifyCreatorPermission } = await import('@/lib/templates/permissions')
const { hasPermission, error: permissionError } = await verifyCreatorPermission(
session.user.id,
data.creatorId,
'member'
)
if (!hasPermission) {
logger.warn(`[${requestId}] User cannot use creator profile: ${data.creatorId}`)
return NextResponse.json({ error: permissionError || 'Access denied' }, { status: 403 })
}
const templateId = uuidv4()
const now = new Date()
// Get the active deployment version for the workflow to copy its state
const activeVersion = await db
.select({
id: workflowDeploymentVersion.id,
state: workflowDeploymentVersion.state,
})
.from(workflowDeploymentVersion)
.where(
and(
eq(workflowDeploymentVersion.workflowId, data.workflowId),
eq(workflowDeploymentVersion.isActive, true)
)
)
.limit(1)
if (activeVersion.length === 0) {
logger.warn(
`[${requestId}] No active deployment version found for workflow: ${data.workflowId}`
)
return NextResponse.json(
{ error: 'Workflow must be deployed before creating a template' },
{ status: 400 }
)
}
// Ensure the state includes workflow variables (if not already included)
let stateWithVariables = activeVersion[0].state as any
if (stateWithVariables && !stateWithVariables.variables) {
// Fetch workflow variables if not in deployment version
const [workflowRecord] = await db
.select({ variables: workflow.variables })
.from(workflow)
.where(eq(workflow.id, data.workflowId))
.limit(1)
stateWithVariables = {
...stateWithVariables,
variables: workflowRecord?.variables || undefined,
}
}
// Extract credential requirements before sanitizing
const requiredCredentials = extractRequiredCredentials(stateWithVariables)
// Sanitize the workflow state to remove all credential values
const sanitizedState = sanitizeWorkflowState(stateWithVariables)
const newTemplate = {
id: templateId,
workflowId: data.workflowId,
name: data.name,
details: data.details || null,
creatorId: data.creatorId,
views: 0,
stars: 0,
status: 'pending' as const, // All new templates start as pending
tags: data.tags || [],
requiredCredentials: requiredCredentials, // Store the extracted credential requirements
state: sanitizedState, // Store the sanitized state without credential values
createdAt: now,
updatedAt: now,
}
await db.insert(templates).values(newTemplate)
logger.info(`[${requestId}] Successfully created template: ${templateId}`)
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.TEMPLATE_CREATED,
resourceType: AuditResourceType.TEMPLATE,
resourceId: templateId,
resourceName: data.name,
description: `Created template "${data.name}"`,
request,
})
return NextResponse.json(
{
id: templateId,
message: 'Template submitted for approval successfully',
},
{ status: 201 }
)
} catch (error: any) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid template data`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid template data', details: error.errors },
{ status: 400 }
)
}
logger.error(`[${requestId}] Error creating template`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}