Skip to content

Commit 7140867

Browse files
committed
Jobs
1 parent 73cd10c commit 7140867

File tree

4 files changed

+518
-275
lines changed

4 files changed

+518
-275
lines changed

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

Lines changed: 152 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
7+
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
78
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
89
import { getSession } from '@/lib/auth'
910
import { generateRequestId } from '@/lib/core/utils/request'
@@ -15,12 +16,80 @@ const logger = createLogger('ScheduleAPI')
1516
export const dynamic = 'force-dynamic'
1617

1718
const scheduleUpdateSchema = z.object({
18-
action: z.literal('reactivate'),
19+
action: z.enum(['reactivate', 'disable']),
1920
})
2021

21-
/**
22-
* Reactivate a disabled schedule
23-
*/
22+
type ScheduleRow = {
23+
id: string
24+
workflowId: string | null
25+
status: string
26+
cronExpression: string | null
27+
timezone: string | null
28+
sourceType: string | null
29+
sourceWorkspaceId: string | null
30+
}
31+
32+
async function fetchAndAuthorize(
33+
requestId: string,
34+
scheduleId: string,
35+
userId: string,
36+
action: 'read' | 'write'
37+
): Promise<
38+
| { schedule: ScheduleRow; workspaceId: string | null }
39+
| NextResponse
40+
> {
41+
const [schedule] = await db
42+
.select({
43+
id: workflowSchedule.id,
44+
workflowId: workflowSchedule.workflowId,
45+
status: workflowSchedule.status,
46+
cronExpression: workflowSchedule.cronExpression,
47+
timezone: workflowSchedule.timezone,
48+
sourceType: workflowSchedule.sourceType,
49+
sourceWorkspaceId: workflowSchedule.sourceWorkspaceId,
50+
})
51+
.from(workflowSchedule)
52+
.where(eq(workflowSchedule.id, scheduleId))
53+
.limit(1)
54+
55+
if (!schedule) {
56+
logger.warn(`[${requestId}] Schedule not found: ${scheduleId}`)
57+
return NextResponse.json({ error: 'Schedule not found' }, { status: 404 })
58+
}
59+
60+
if (schedule.sourceType === 'job') {
61+
if (!schedule.sourceWorkspaceId) {
62+
return NextResponse.json({ error: 'Job has no workspace' }, { status: 400 })
63+
}
64+
const allowed = await verifyWorkspaceMembership(userId, schedule.sourceWorkspaceId)
65+
if (!allowed) {
66+
return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
67+
}
68+
return { schedule, workspaceId: schedule.sourceWorkspaceId }
69+
}
70+
71+
const authorization = await authorizeWorkflowByWorkspacePermission({
72+
workflowId: schedule.workflowId,
73+
userId,
74+
action,
75+
})
76+
77+
if (!authorization.workflow) {
78+
logger.warn(`[${requestId}] Workflow not found for schedule: ${scheduleId}`)
79+
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
80+
}
81+
82+
if (!authorization.allowed) {
83+
logger.warn(`[${requestId}] User not authorized to modify schedule: ${scheduleId}`)
84+
return NextResponse.json(
85+
{ error: authorization.message || 'Not authorized to modify this schedule' },
86+
{ status: authorization.status }
87+
)
88+
}
89+
90+
return { schedule, workspaceId: authorization.workflow.workspaceId ?? null }
91+
}
92+
2493
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
2594
const requestId = generateRequestId()
2695

@@ -40,44 +109,43 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
40109
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
41110
}
42111

43-
const [schedule] = await db
44-
.select({
45-
id: workflowSchedule.id,
46-
workflowId: workflowSchedule.workflowId,
47-
status: workflowSchedule.status,
48-
cronExpression: workflowSchedule.cronExpression,
49-
timezone: workflowSchedule.timezone,
112+
const result = await fetchAndAuthorize(requestId, scheduleId, session.user.id, 'write')
113+
if (result instanceof NextResponse) return result
114+
const { schedule, workspaceId } = result
115+
116+
const { action } = validation.data
117+
118+
if (action === 'disable') {
119+
if (schedule.status === 'disabled') {
120+
return NextResponse.json({ message: 'Schedule is already disabled' })
121+
}
122+
123+
await db
124+
.update(workflowSchedule)
125+
.set({ status: 'disabled', nextRunAt: null, updatedAt: new Date() })
126+
.where(eq(workflowSchedule.id, scheduleId))
127+
128+
logger.info(`[${requestId}] Disabled schedule: ${scheduleId}`)
129+
130+
recordAudit({
131+
workspaceId,
132+
actorId: session.user.id,
133+
action: AuditAction.SCHEDULE_UPDATED,
134+
resourceType: AuditResourceType.SCHEDULE,
135+
resourceId: scheduleId,
136+
actorName: session.user.name ?? undefined,
137+
actorEmail: session.user.email ?? undefined,
138+
description: `Disabled schedule ${scheduleId}`,
139+
metadata: {},
140+
request,
50141
})
51-
.from(workflowSchedule)
52-
.where(eq(workflowSchedule.id, scheduleId))
53-
.limit(1)
54142

55-
if (!schedule) {
56-
logger.warn(`[${requestId}] Schedule not found: ${scheduleId}`)
57-
return NextResponse.json({ error: 'Schedule not found' }, { status: 404 })
58-
}
59-
60-
const authorization = await authorizeWorkflowByWorkspacePermission({
61-
workflowId: schedule.workflowId,
62-
userId: session.user.id,
63-
action: 'write',
64-
})
65-
66-
if (!authorization.workflow) {
67-
logger.warn(`[${requestId}] Workflow not found for schedule: ${scheduleId}`)
68-
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
69-
}
70-
71-
if (!authorization.allowed) {
72-
logger.warn(`[${requestId}] User not authorized to modify this schedule: ${scheduleId}`)
73-
return NextResponse.json(
74-
{ error: authorization.message || 'Not authorized to modify this schedule' },
75-
{ status: authorization.status }
76-
)
143+
return NextResponse.json({ message: 'Schedule disabled successfully' })
77144
}
78145

146+
// reactivate
79147
if (schedule.status === 'active') {
80-
return NextResponse.json({ message: 'Schedule is already active' }, { status: 200 })
148+
return NextResponse.json({ message: 'Schedule is already active' })
81149
}
82150

83151
if (!schedule.cronExpression) {
@@ -96,35 +164,70 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
96164

97165
await db
98166
.update(workflowSchedule)
99-
.set({
100-
status: 'active',
101-
failedCount: 0,
102-
updatedAt: now,
103-
nextRunAt,
104-
})
167+
.set({ status: 'active', failedCount: 0, updatedAt: now, nextRunAt })
105168
.where(eq(workflowSchedule.id, scheduleId))
106169

107170
logger.info(`[${requestId}] Reactivated schedule: ${scheduleId}`)
108171

109172
recordAudit({
110-
workspaceId: authorization.workflow.workspaceId ?? null,
173+
workspaceId,
111174
actorId: session.user.id,
112175
action: AuditAction.SCHEDULE_UPDATED,
113176
resourceType: AuditResourceType.SCHEDULE,
114177
resourceId: scheduleId,
115178
actorName: session.user.name ?? undefined,
116179
actorEmail: session.user.email ?? undefined,
117-
description: `Reactivated schedule for workflow ${schedule.workflowId}`,
180+
description: `Reactivated schedule ${scheduleId}`,
118181
metadata: { cronExpression: schedule.cronExpression, timezone: schedule.timezone },
119182
request,
120183
})
121184

122-
return NextResponse.json({
123-
message: 'Schedule activated successfully',
124-
nextRunAt,
125-
})
185+
return NextResponse.json({ message: 'Schedule activated successfully', nextRunAt })
126186
} catch (error) {
127187
logger.error(`[${requestId}] Error updating schedule`, error)
128188
return NextResponse.json({ error: 'Failed to update schedule' }, { status: 500 })
129189
}
130190
}
191+
192+
export async function DELETE(
193+
request: NextRequest,
194+
{ params }: { params: Promise<{ id: string }> }
195+
) {
196+
const requestId = generateRequestId()
197+
198+
try {
199+
const { id: scheduleId } = await params
200+
201+
const session = await getSession()
202+
if (!session?.user?.id) {
203+
logger.warn(`[${requestId}] Unauthorized schedule delete attempt`)
204+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
205+
}
206+
207+
const result = await fetchAndAuthorize(requestId, scheduleId, session.user.id, 'write')
208+
if (result instanceof NextResponse) return result
209+
const { schedule, workspaceId } = result
210+
211+
await db.delete(workflowSchedule).where(eq(workflowSchedule.id, scheduleId))
212+
213+
logger.info(`[${requestId}] Deleted schedule: ${scheduleId}`)
214+
215+
recordAudit({
216+
workspaceId,
217+
actorId: session.user.id,
218+
action: AuditAction.SCHEDULE_UPDATED,
219+
resourceType: AuditResourceType.SCHEDULE,
220+
resourceId: scheduleId,
221+
actorName: session.user.name ?? undefined,
222+
actorEmail: session.user.email ?? undefined,
223+
description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} ${scheduleId}`,
224+
metadata: {},
225+
request,
226+
})
227+
228+
return NextResponse.json({ message: 'Schedule deleted successfully' })
229+
} catch (error) {
230+
logger.error(`[${requestId}] Error deleting schedule`, error)
231+
return NextResponse.json({ error: 'Failed to delete schedule' }, { status: 500 })
232+
}
233+
}

0 commit comments

Comments
 (0)