@@ -43,7 +43,7 @@ pub(crate) fn validate_tutorial_id(id: &str) -> Result<(), String> {
4343 }
4444
4545 // Ensure only safe characters for database and URL usage
46- if !id. chars ( ) . all ( |c| c. is_alphanumeric ( ) || c == '-' ) {
46+ if !id. chars ( ) . all ( |c| c. is_alphanumeric ( ) || c == '-' || c == '_' || c == '.' ) {
4747 return Err ( "Tutorial ID contains invalid characters" . to_string ( ) ) ;
4848 }
4949 Ok ( ( ) )
@@ -308,7 +308,38 @@ pub async fn create_tutorial(
308308 return Err ( ( StatusCode :: BAD_REQUEST , Json ( ErrorResponse { error : e } ) ) ) ;
309309 }
310310
311- let id = Uuid :: new_v4 ( ) . to_string ( ) ;
311+ let id = if let Some ( custom_id) = & payload. id {
312+ let trimmed = custom_id. trim ( ) ;
313+ if let Err ( e) = validate_tutorial_id ( trimmed) {
314+ return Err ( ( StatusCode :: BAD_REQUEST , Json ( ErrorResponse { error : e } ) ) ) ;
315+ }
316+ // Check for collision
317+ let exists: Option < ( i64 , ) > = sqlx:: query_as ( "SELECT 1 FROM tutorials WHERE id = ?" )
318+ . bind ( trimmed)
319+ . fetch_optional ( & pool)
320+ . await
321+ . map_err ( |e| {
322+ tracing:: error!( "Database error checking ID existence: {}" , e) ;
323+ (
324+ StatusCode :: INTERNAL_SERVER_ERROR ,
325+ Json ( ErrorResponse {
326+ error : "Failed to create tutorial" . to_string ( ) ,
327+ } ) ,
328+ )
329+ } ) ?;
330+
331+ if exists. is_some ( ) {
332+ return Err ( (
333+ StatusCode :: CONFLICT ,
334+ Json ( ErrorResponse {
335+ error : "Tutorial ID already exists" . to_string ( ) ,
336+ } ) ,
337+ ) ) ;
338+ }
339+ trimmed. to_string ( )
340+ } else {
341+ Uuid :: new_v4 ( ) . to_string ( )
342+ } ;
312343 let sanitized_topics = sanitize_topics ( & payload. topics )
313344 . map_err ( |e| ( StatusCode :: BAD_REQUEST , Json ( ErrorResponse { error : e } ) ) ) ?;
314345 let topics_json = serde_json:: to_string ( & sanitized_topics) . map_err ( |e| {
0 commit comments