Refactor code structure for improved readability and maintainability

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-27 13:55:06 -04:00
parent 8d606ddefc
commit 7a2afce796
10 changed files with 302 additions and 15 deletions
+76
View File
@@ -4,6 +4,7 @@ use axum::{
http::{HeaderMap, header::AUTHORIZATION},
http::StatusCode,
response::IntoResponse,
response::sse::{Event, KeepAlive, Sse},
Extension,
};
use aws_config::BehaviorVersion;
@@ -4706,3 +4707,78 @@ fn extract_block_content(metadata: &Option<serde_json::Value>) -> String {
}
block_content
}
// ─── SSE: Pizarra Colaborativa en Tiempo Real (Fase 37) ──────────────────────
pub async fn stream_lesson_collaborative_canvas(
Org(org_ctx): Org,
State(pool): State<PgPool>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, StatusCode> {
use std::convert::Infallible;
use tokio_stream::wrappers::ReceiverStream;
let lesson_exists = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM lessons WHERE id = $1 AND organization_id = $2)",
)
.bind(id)
.bind(org_ctx.id)
.fetch_one(&pool)
.await
.map_err(|e| {
tracing::error!("stream_lesson_collaborative_canvas: lesson check failed: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
if !lesson_exists {
return Err(StatusCode::NOT_FOUND);
}
let (tx, rx) = tokio::sync::mpsc::channel::<Result<Event, Infallible>>(16);
tokio::spawn(async move {
#[derive(sqlx::FromRow)]
struct CanvasRow {
canvas_state: serde_json::Value,
revision: i64,
updated_at: DateTime<Utc>,
}
let mut last_revision: i64 = -1;
loop {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let result = sqlx::query_as::<_, CanvasRow>(
"SELECT canvas_state, revision, updated_at FROM lesson_collaborative_canvases WHERE lesson_id = $1 AND organization_id = $2",
)
.bind(id)
.bind(org_ctx.id)
.fetch_optional(&pool)
.await;
match result {
Ok(Some(row)) if row.revision != last_revision => {
last_revision = row.revision;
let payload = serde_json::json!({
"lesson_id": id,
"canvas_state": row.canvas_state,
"revision": row.revision,
"updated_at": row.updated_at.to_rfc3339(),
});
if tx.send(Ok(Event::default().data(payload.to_string()))).await.is_err() {
break; // Cliente desconectado
}
}
Ok(_) => {}
Err(e) => {
tracing::error!("stream_lesson_collaborative_canvas: poll error: {}", e);
let _ = tx.send(Ok(Event::default().event("error").data("poll_error"))).await;
}
}
}
});
Ok(Sse::new(ReceiverStream::new(rx))
.keep_alive(KeepAlive::default()))
}
@@ -430,3 +430,66 @@ pub async fn lti_grade_passback(
normalized_score: normalized,
}))
}
// ─── Rotación de Secreto LTI (Fase 37) ───────────────────────────────────────
#[derive(Debug, Serialize)]
pub struct RotateSecretResponse {
pub tool_id: Uuid,
pub new_secret: String,
pub rotated_at: chrono::DateTime<chrono::Utc>,
}
pub async fn rotate_lti_tool_secret(
Org(org_ctx): Org,
_claims: Claims,
State(pool): State<PgPool>,
Path((course_id, tool_id)): Path<(Uuid, Uuid)>,
) -> Result<Json<RotateSecretResponse>, (StatusCode, String)> {
use rand::Rng;
// Verificar que la herramienta pertenece al curso y organización
let tool_exists = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS(SELECT 1 FROM lti_external_tools WHERE id = $1 AND course_id = $2 AND organization_id = $3)",
)
.bind(tool_id)
.bind(course_id)
.bind(org_ctx.id)
.fetch_one(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
if !tool_exists {
return Err((StatusCode::NOT_FOUND, "Herramienta LTI no encontrada".to_string()));
}
// Generar nuevo secreto aleatorio de 32 caracteres alfanuméricos
let new_secret: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(32)
.map(char::from)
.collect();
let now = chrono::Utc::now();
sqlx::query(
"UPDATE lti_external_tools SET shared_secret = $1, updated_at = $2 WHERE id = $3",
)
.bind(&new_secret)
.bind(now)
.bind(tool_id)
.execute(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
tracing::info!(
"rotate_lti_tool_secret: rotated secret for tool {} in course {} org {}",
tool_id, course_id, org_ctx.id
);
Ok(Json(RotateSecretResponse {
tool_id,
new_secret,
rotated_at: now,
}))
}
+8
View File
@@ -168,6 +168,10 @@ async fn main() {
get(handlers::get_lesson_collaborative_canvas)
.put(handlers::update_lesson_collaborative_canvas),
)
.route(
"/lessons/{id}/collaborative-canvas/stream",
get(handlers::stream_lesson_collaborative_canvas),
)
.route("/lessons/{id}/bookmark", post(handlers::toggle_bookmark))
.route("/bookmarks", get(handlers::get_user_bookmarks))
.route("/grades", post(handlers::submit_lesson_score))
@@ -210,6 +214,10 @@ async fn main() {
put(handlers_lti_consumer::update_course_lti_tool)
.delete(handlers_lti_consumer::delete_course_lti_tool),
)
.route(
"/courses/{id}/lti-tools/{tool_id}/rotate-secret",
post(handlers_lti_consumer::rotate_lti_tool_secret),
)
// Portafolio e insignias (Badges)
.route("/profile/{user_id}", get(portfolio::get_public_profile))
.route("/my/badges", get(portfolio::get_my_badges))