11use std:: sync:: Arc ;
2+ use tokio:: fs;
23
34use axum:: {
45 extract:: { Path , Query , State } ,
@@ -9,13 +10,30 @@ use axum::{
910use serde:: { Deserialize , Serialize } ;
1011
1112use crate :: routes:: errors:: ErrorResponse ;
13+ use crate :: routes:: recipe_utils:: validate_recipe;
1214use crate :: state:: AppState ;
13- use goose:: scheduler:: ScheduledJob ;
15+ use goose:: recipe:: Recipe ;
16+ use goose:: scheduler:: { get_default_scheduled_recipes_dir, ScheduledJob } ;
17+
18+ fn validate_schedule_id ( id : & str ) -> Result < ( ) , ErrorResponse > {
19+ let is_valid = !id. is_empty ( )
20+ && id
21+ . chars ( )
22+ . all ( |c| c. is_ascii_alphanumeric ( ) || c == '-' || c == '_' || c == ' ' ) ;
23+
24+ if !is_valid {
25+ return Err ( ErrorResponse :: bad_request (
26+ "Schedule name must use only alphanumeric characters, hyphens, underscores, or spaces"
27+ . to_string ( ) ,
28+ ) ) ;
29+ }
30+ Ok ( ( ) )
31+ }
1432
1533#[ derive( Deserialize , Serialize , utoipa:: ToSchema ) ]
1634pub struct CreateScheduleRequest {
1735 id : String ,
18- recipe_source : String ,
36+ recipe : Recipe ,
1937 cron : String ,
2038}
2139
@@ -89,20 +107,47 @@ async fn create_schedule(
89107 State ( state) : State < Arc < AppState > > ,
90108 Json ( req) : Json < CreateScheduleRequest > ,
91109) -> Result < Json < ScheduledJob > , ErrorResponse > {
92- let scheduler = state. scheduler ( ) ;
110+ let id = req. id . trim ( ) . to_string ( ) ;
111+ validate_schedule_id ( & id) ?;
112+
113+ if req. recipe . check_for_security_warnings ( ) {
114+ return Err ( ErrorResponse :: bad_request (
115+ "This recipe contains hidden characters that could be malicious. Please remove them before trying to save." . to_string ( ) ,
116+ ) ) ;
117+ }
118+ if let Err ( err) = validate_recipe ( & req. recipe ) {
119+ return Err ( ErrorResponse {
120+ message : err. message ,
121+ status : err. status ,
122+ } ) ;
123+ }
124+ let scheduled_recipes_dir = get_default_scheduled_recipes_dir ( ) . map_err ( |e| {
125+ ErrorResponse :: internal ( format ! ( "Failed to get scheduled recipes directory: {}" , e) )
126+ } ) ?;
127+
128+ let recipe_path = scheduled_recipes_dir. join ( format ! ( "{}.yaml" , id) ) ;
129+ let yaml_content = req
130+ . recipe
131+ . to_yaml ( )
132+ . map_err ( |e| ErrorResponse :: internal ( format ! ( "Failed to convert recipe to YAML: {}" , e) ) ) ?;
133+ fs:: write ( & recipe_path, yaml_content)
134+ . await
135+ . map_err ( |e| ErrorResponse :: internal ( format ! ( "Failed to save recipe file: {}" , e) ) ) ?;
93136
94137 let job = ScheduledJob {
95- id : req . id ,
96- source : req . recipe_source ,
138+ id,
139+ source : recipe_path . to_string_lossy ( ) . into_owned ( ) ,
97140 cron : req. cron ,
98141 last_run : None ,
99142 currently_running : false ,
100143 paused : false ,
101144 current_session_id : None ,
102145 process_start_time : None ,
103146 } ;
147+
148+ let scheduler = state. scheduler ( ) ;
104149 scheduler
105- . add_scheduled_job ( job. clone ( ) , true )
150+ . add_scheduled_job ( job. clone ( ) , false )
106151 . await
107152 . map_err ( |e| match e {
108153 goose:: scheduler:: SchedulerError :: CronParseError ( msg) => {
@@ -117,6 +162,7 @@ async fn create_schedule(
117162 } ,
118163 _ => ErrorResponse :: internal ( format ! ( "Error creating schedule: {}" , e) ) ,
119164 } ) ?;
165+
120166 Ok ( Json ( job) )
121167}
122168
0 commit comments