Files
openccb/services/cms-service/src/handlers.rs
T

334 lines
11 KiB
Rust

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<Uuid>,
}
#[derive(Deserialize)]
pub struct LessonQuery {
pub module_id: Option<Uuid>,
}
pub async fn create_course(
State(pool): State<PgPool>,
Json(payload): Json<serde_json::Value>,
) -> Result<Json<Course>, 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<PgPool>,
) -> Result<Json<Vec<Course>>, 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<PgPool>,
Json(payload): Json<serde_json::Value>,
) -> Result<Json<Module>, 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<PgPool>,
Json(payload): Json<serde_json::Value>,
) -> Result<Json<Lesson>, 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<PgPool>,
Path(id): Path<Uuid>,
) -> Result<Json<Lesson>, 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<PgPool>,
Path(id): Path<Uuid>,
) -> Result<Json<Lesson>, 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<PgPool>,
Path(id): Path<Uuid>,
Json(payload): Json<serde_json::Value>,
) -> Result<Json<Lesson>, 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<PgPool>,
Path(id): Path<Uuid>,
) -> Result<Json<Course>, 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<PgPool>,
Query(query): Query<ModuleQuery>,
) -> Result<Json<Vec<Module>>, 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<PgPool>,
Query(query): Query<LessonQuery>,
) -> Result<Json<Vec<Lesson>>, 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<PgPool>,
mut multipart: axum::extract::Multipart,
) -> Result<Json<UploadResponse>, (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,
}))
}