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

223 lines
7.0 KiB
Rust

use axum::{
Json,
extract::State,
http::StatusCode,
};
use common::auth::Claims;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::PgPool;
use uuid::Uuid;
use super::handlers::{log_action, Org};
#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
pub struct OrganizationExerciseSettings {
pub organization_id: Uuid,
pub audio_response_enabled: bool,
pub hotspot_enabled: bool,
pub memory_match_enabled: bool,
pub peer_review_enabled: bool,
pub role_playing_enabled: bool,
pub mermaid_enabled: bool,
pub code_lab_enabled: bool,
pub certificates_enabled: bool,
}
impl OrganizationExerciseSettings {
pub fn defaults(organization_id: Uuid) -> Self {
Self {
organization_id,
audio_response_enabled: true,
hotspot_enabled: true,
memory_match_enabled: true,
peer_review_enabled: true,
role_playing_enabled: true,
mermaid_enabled: false,
code_lab_enabled: true,
certificates_enabled: true,
}
}
pub fn is_enabled(&self, feature: &str) -> bool {
match feature {
"audio-response" => self.audio_response_enabled,
"hotspot" => self.hotspot_enabled,
"memory-match" => self.memory_match_enabled,
"peer-review" => self.peer_review_enabled,
"role-playing" => self.role_playing_enabled,
"mermaid" => self.mermaid_enabled,
"code-lab" => self.code_lab_enabled,
"certificates" => self.certificates_enabled,
_ => true,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateOrganizationExerciseSettingsPayload {
pub audio_response_enabled: bool,
pub hotspot_enabled: bool,
pub memory_match_enabled: bool,
pub peer_review_enabled: bool,
pub role_playing_enabled: bool,
pub mermaid_enabled: bool,
pub code_lab_enabled: bool,
pub certificates_enabled: bool,
}
pub async fn load_organization_exercise_settings(
pool: &PgPool,
organization_id: Uuid,
) -> Result<OrganizationExerciseSettings, sqlx::Error> {
let settings = sqlx::query_as::<_, OrganizationExerciseSettings>(
r#"
SELECT
organization_id,
audio_response_enabled,
hotspot_enabled,
memory_match_enabled,
peer_review_enabled,
role_playing_enabled,
mermaid_enabled,
code_lab_enabled,
certificates_enabled
FROM organization_exercise_settings
WHERE organization_id = $1
"#,
)
.bind(organization_id)
.fetch_optional(pool)
.await?;
Ok(settings.unwrap_or_else(|| OrganizationExerciseSettings::defaults(organization_id)))
}
async fn upsert_organization_exercise_settings(
pool: &PgPool,
organization_id: Uuid,
payload: &UpdateOrganizationExerciseSettingsPayload,
) -> Result<OrganizationExerciseSettings, sqlx::Error> {
let mut tx = pool.begin().await?;
let settings = sqlx::query_as::<_, OrganizationExerciseSettings>(
r#"
INSERT INTO organization_exercise_settings (
organization_id,
audio_response_enabled,
hotspot_enabled,
memory_match_enabled,
peer_review_enabled,
role_playing_enabled,
mermaid_enabled,
code_lab_enabled,
certificates_enabled,
updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())
ON CONFLICT (organization_id) DO UPDATE SET
audio_response_enabled = EXCLUDED.audio_response_enabled,
hotspot_enabled = EXCLUDED.hotspot_enabled,
memory_match_enabled = EXCLUDED.memory_match_enabled,
peer_review_enabled = EXCLUDED.peer_review_enabled,
role_playing_enabled = EXCLUDED.role_playing_enabled,
mermaid_enabled = EXCLUDED.mermaid_enabled,
code_lab_enabled = EXCLUDED.code_lab_enabled,
certificates_enabled = EXCLUDED.certificates_enabled,
updated_at = NOW()
RETURNING
organization_id,
audio_response_enabled,
hotspot_enabled,
memory_match_enabled,
peer_review_enabled,
role_playing_enabled,
mermaid_enabled,
code_lab_enabled,
certificates_enabled
"#,
)
.bind(organization_id)
.bind(payload.audio_response_enabled)
.bind(payload.hotspot_enabled)
.bind(payload.memory_match_enabled)
.bind(payload.peer_review_enabled)
.bind(payload.role_playing_enabled)
.bind(payload.mermaid_enabled)
.bind(payload.code_lab_enabled)
.bind(payload.certificates_enabled)
.fetch_one(&mut *tx)
.await?;
// Sincronizar con la tabla organizations para que el LMS reciba el valor correcto al publicar
sqlx::query(
"UPDATE organizations SET certificates_enabled = $1, updated_at = NOW() WHERE id = $2"
)
.bind(payload.certificates_enabled)
.bind(organization_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(settings)
}
pub async fn get_organization_exercise_settings(
Org(org_ctx): Org,
State(pool): State<PgPool>,
) -> Result<Json<OrganizationExerciseSettings>, (StatusCode, String)> {
let settings = load_organization_exercise_settings(&pool, org_ctx.id)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error al cargar configuración de ejercicios: {}", e),
)
})?;
Ok(Json(settings))
}
pub async fn update_organization_exercise_settings(
claims: Claims,
Org(org_ctx): Org,
State(pool): State<PgPool>,
Json(payload): Json<UpdateOrganizationExerciseSettingsPayload>,
) -> Result<Json<OrganizationExerciseSettings>, (StatusCode, String)> {
if claims.role != "admin" {
return Err((StatusCode::FORBIDDEN, "Se requiere acceso de administrador".into()));
}
let settings = upsert_organization_exercise_settings(&pool, org_ctx.id, &payload)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error al guardar configuración de ejercicios: {}", e),
)
})?;
log_action(
&pool,
claims.org,
claims.sub,
"UPDATE_EXERCISE_SETTINGS",
"Organization",
org_ctx.id,
json!({
"audio_response_enabled": settings.audio_response_enabled,
"hotspot_enabled": settings.hotspot_enabled,
"memory_match_enabled": settings.memory_match_enabled,
"peer_review_enabled": settings.peer_review_enabled,
"role_playing_enabled": settings.role_playing_enabled,
"mermaid_enabled": settings.mermaid_enabled,
"code_lab_enabled": settings.code_lab_enabled,
"certificates_enabled": settings.certificates_enabled,
}),
)
.await;
Ok(Json(settings))
}