use axum::{ extract::{State, Path, Query}, http::StatusCode, Json, }; use common::models::{Course, Module, Lesson}; use sqlx::PgPool; use uuid::Uuid; use serde_json::json; use serde::{Deserialize, Serialize}; #[derive(Deserialize)] pub struct ModuleQuery { pub course_id: Option, } #[derive(Deserialize)] pub struct LessonQuery { pub module_id: Option, } pub async fn create_course( State(pool): State, Json(payload): Json, ) -> Result, StatusCode> { let title = payload.get("title").and_then(|t| t.as_str()).ok_or(StatusCode::BAD_REQUEST)?; let instructor_id = Uuid::new_v4(); let course = sqlx::query_as::<_, Course>( "INSERT INTO courses (title, instructor_id) VALUES ($1, $2) RETURNING *" ) .bind(title) .bind(instructor_id) .fetch_one(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; log_action(&pool, instructor_id, "CREATE", "Course", course.id, json!({ "title": title })).await; Ok(Json(course)) } pub async fn get_courses( State(pool): State, ) -> Result>, StatusCode> { let courses = sqlx::query_as::<_, Course>("SELECT * FROM courses") .fetch_all(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(courses)) } pub async fn create_module( State(pool): State, Json(payload): Json, ) -> Result, StatusCode> { let title = payload.get("title").and_then(|t| t.as_str()).ok_or(StatusCode::BAD_REQUEST)?; let course_id_str = payload.get("course_id").and_then(|v| v.as_str()).ok_or(StatusCode::BAD_REQUEST)?; let course_id = Uuid::parse_str(course_id_str).map_err(|_| StatusCode::BAD_REQUEST)?; let position = payload.get("position").and_then(|v| v.as_i64()).unwrap_or(0) as i32; let module = sqlx::query_as::<_, Module>( "INSERT INTO modules (course_id, title, position) VALUES ($1, $2, $3) RETURNING *" ) .bind(course_id) .bind(title) .bind(position) .fetch_one(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; log_action(&pool, Uuid::new_v4(), "CREATE", "Module", module.id, json!({ "title": title, "course_id": course_id })).await; Ok(Json(module)) } pub async fn create_lesson( State(pool): State, Json(payload): Json, ) -> Result, StatusCode> { let title = payload.get("title").and_then(|t| t.as_str()).ok_or(StatusCode::BAD_REQUEST)?; let module_id_str = payload.get("module_id").and_then(|v| v.as_str()).ok_or(StatusCode::BAD_REQUEST)?; let module_id = Uuid::parse_str(module_id_str).map_err(|_| StatusCode::BAD_REQUEST)?; let content_type = payload.get("content_type").and_then(|t| t.as_str()).ok_or(StatusCode::BAD_REQUEST)?; let content_url = payload.get("content_url").and_then(|v| v.as_str()); let position = payload.get("position").and_then(|v| v.as_i64()).unwrap_or(0) as i32; let transcription = payload.get("transcription").cloned(); let metadata = payload.get("metadata").cloned(); let lesson = sqlx::query_as::<_, Lesson>( "INSERT INTO lessons (module_id, title, content_type, content_url, position, transcription, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *" ) .bind(module_id) .bind(title) .bind(content_type) .bind(content_url) .bind(position) .bind(transcription) .bind(metadata) .fetch_one(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; log_action(&pool, Uuid::new_v4(), "CREATE", "Lesson", lesson.id, json!({ "title": title, "module_id": module_id })).await; Ok(Json(lesson)) } pub async fn process_transcription( State(pool): State, Path(id): Path, ) -> Result, StatusCode> { // 1. Fetch lesson let _lesson = sqlx::query_as::<_, Lesson>("SELECT * FROM lessons WHERE id = $1") .bind(id) .fetch_one(&pool) .await .map_err(|_| StatusCode::NOT_FOUND)?; // 2. Simulate AI Processing let mock_transcription = json!({ "en": "This is a simulated transcription of the video content in English.", "es": "Esta es una transcripción simulada del contenido del video en español.", "cues": [ { "start": 0.0, "end": 2.0, "text": "Hello world!" }, { "start": 2.1, "end": 5.0, "text": "Welcome to OpenCCB." } ] }); // 3. Update lesson let updated_lesson = sqlx::query_as::<_, Lesson>( "UPDATE lessons SET transcription = $1 WHERE id = $2 RETURNING *" ) .bind(mock_transcription) .bind(id) .fetch_one(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; log_action(&pool, Uuid::new_v4(), "TRANSCRIPTION_PROCESSED", "Lesson", id, json!({})).await; Ok(Json(updated_lesson)) } pub async fn get_lesson( State(pool): State, Path(id): Path, ) -> Result, StatusCode> { let lesson = sqlx::query_as::<_, Lesson>("SELECT * FROM lessons WHERE id = $1") .bind(id) .fetch_one(&pool) .await .map_err(|_| StatusCode::NOT_FOUND)?; Ok(Json(lesson)) } pub async fn update_lesson( State(pool): State, Path(id): Path, Json(payload): Json, ) -> Result, StatusCode> { let title = payload.get("title").and_then(|t| t.as_str()); let content_type = payload.get("content_type").and_then(|t| t.as_str()); let content_url = payload.get("content_url").and_then(|t| t.as_str()); let position = payload.get("position").and_then(|v| v.as_i64()).map(|v| v as i32); let updated_lesson = sqlx::query_as::<_, Lesson>( "UPDATE lessons SET title = COALESCE($1, title), content_type = COALESCE($2, content_type), content_url = COALESCE($3, content_url), position = COALESCE($4, position) WHERE id = $5 RETURNING *" ) .bind(title) .bind(content_type) .bind(content_url) .bind(position) .bind(id) .fetch_one(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; log_action(&pool, Uuid::new_v4(), "UPDATE", "Lesson", id, json!(payload)).await; Ok(Json(updated_lesson)) } async fn log_action( pool: &PgPool, user_id: Uuid, action: &str, entity_type: &str, entity_id: Uuid, changes: serde_json::Value, ) { let _ = sqlx::query( "INSERT INTO audit_logs (user_id, action, entity_type, entity_id, changes) VALUES ($1, $2, $3, $4, $5)" ) .bind(user_id) .bind(action) .bind(entity_type) .bind(entity_id) .bind(changes) .execute(pool) .await; } pub async fn get_course( State(pool): State, Path(id): Path, ) -> Result, StatusCode> { let course = sqlx::query_as::<_, Course>("SELECT * FROM courses WHERE id = $1") .bind(id) .fetch_one(&pool) .await .map_err(|_| StatusCode::NOT_FOUND)?; Ok(Json(course)) } pub async fn get_modules( State(pool): State, Query(query): Query, ) -> Result>, StatusCode> { let modules = match query.course_id { Some(course_id) => { sqlx::query_as::<_, Module>("SELECT * FROM modules WHERE course_id = $1 ORDER BY position") .bind(course_id) .fetch_all(&pool) .await } None => { sqlx::query_as::<_, Module>("SELECT * FROM modules ORDER BY position") .fetch_all(&pool) .await } } .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(modules)) } pub async fn get_lessons( State(pool): State, Query(query): Query, ) -> Result>, StatusCode> { let lessons = match query.module_id { Some(module_id) => { sqlx::query_as::<_, Lesson>("SELECT * FROM lessons WHERE module_id = $1 ORDER BY position") .bind(module_id) .fetch_all(&pool) .await } None => { sqlx::query_as::<_, Lesson>("SELECT * FROM lessons ORDER BY position") .fetch_all(&pool) .await } } .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(lessons)) } #[derive(Debug, Serialize)] pub struct UploadResponse { pub id: Uuid, pub filename: String, pub url: String, } pub async fn upload_asset( State(pool): State, mut multipart: axum::extract::Multipart, ) -> Result, (StatusCode, String)> { let mut filename = String::new(); let mut data = Vec::new(); let mut mimetype = String::new(); while let Some(field) = multipart.next_field().await.map_err(|e: axum::extract::multipart::MultipartError| (StatusCode::BAD_REQUEST, e.to_string()))? { let name = field.name().unwrap_or_default().to_string(); if name == "file" { filename = field.file_name().unwrap_or("unnamed").to_string(); mimetype = field.content_type().unwrap_or("application/octet-stream").to_string(); data = field.bytes().await.map_err(|e: axum::extract::multipart::MultipartError| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?.to_vec(); } } if data.is_empty() { return Err((StatusCode::BAD_REQUEST, "No file uploaded".to_string())); } let asset_id = Uuid::new_v4(); let extension = std::path::Path::new(&filename) .extension() .and_then(|s| s.to_str()) .unwrap_or(""); let storage_filename = format!("{}.{}", asset_id, extension); let storage_path = format!("uploads/{}", storage_filename); // Ensure uploads directory exists tokio::fs::create_dir_all("uploads").await.map_err(|e: std::io::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; // Write file tokio::fs::write(&storage_path, data).await.map_err(|e: std::io::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; // Record in DB let size_bytes = tokio::fs::metadata(&storage_path).await.map(|m| m.len() as i64).unwrap_or(0); sqlx::query( "INSERT INTO assets (id, filename, storage_path, mimetype, size_bytes) VALUES ($1, $2, $3, $4, $5)" ) .bind(asset_id) .bind(&filename) .bind(storage_path) .bind(mimetype) .bind(size_bytes) .execute(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let url = format!("/assets/{}", storage_filename); Ok(Json(UploadResponse { id: asset_id, filename, url, })) }