@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44import { eq } from 'drizzle-orm'
55import { type NextRequest , NextResponse } from 'next/server'
66import { z } from 'zod'
7+ import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
78import { AuditAction , AuditResourceType , recordAudit } from '@/lib/audit/log'
89import { getSession } from '@/lib/auth'
910import { generateRequestId } from '@/lib/core/utils/request'
@@ -15,12 +16,80 @@ const logger = createLogger('ScheduleAPI')
1516export const dynamic = 'force-dynamic'
1617
1718const 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+
2493export 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