feat: add LTI 1.3 Tool Consumer support with database migrations and API endpoints

- Implemented database migrations for lti_external_tools and lti_grade_passback_events tables in both cms-service and lms-service.
- Created API handlers for managing LTI tools including listing, creating, updating, and deleting tools.
- Added functionality for LTI grade passback with validation and signature verification.
- Developed frontend components for LTI tool management and display in course editor.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-27 12:51:13 -04:00
parent f6a3f6aedf
commit fef731df72
16 changed files with 965 additions and 12 deletions
@@ -23,6 +23,7 @@ import PeerReviewPlayer from "@/components/blocks/PeerReviewPlayer";
import MermaidViewer from "@/components/blocks/MermaidViewer";
import ScormPlayer from "@/components/blocks/ScormPlayer";
import PluginBlock from "@/components/blocks/PluginBlock";
import LtiToolPlayer from "@/components/blocks/LtiToolPlayer";
import InteractiveTranscript from "@/components/InteractiveTranscript";
import AITutor from "@/components/AITutor";
import LessonLockedView from "@/components/LessonLockedView";
@@ -542,6 +543,13 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les
launchUrl={block.launch_url || block.url || lesson.content_url || ""}
/>
);
case 'lti-tool':
return (
<LtiToolPlayer
title={block.title || 'Herramienta LTI'}
launchUrl={block.launch_url || block.url || ''}
/>
);
default:
return <div className="p-4 bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-xl text-xs font-bold text-gray-600 dark:text-gray-500 uppercase tracking-widest">Tipo de Bloque Desconocido: {block.type}</div>;
}
@@ -0,0 +1,48 @@
"use client";
import React from "react";
import { ExternalLink, ShieldCheck } from "lucide-react";
interface LtiToolPlayerProps {
title: string;
launchUrl: string;
}
export default function LtiToolPlayer({ title, launchUrl }: LtiToolPlayerProps) {
if (!launchUrl || !launchUrl.startsWith("https://")) {
return (
<div className="rounded-xl border border-red-200 dark:border-red-900/40 bg-red-50 dark:bg-red-900/20 p-4 text-sm text-red-700 dark:text-red-300">
La herramienta LTI no tiene una URL segura (HTTPS) válida.
</div>
);
}
return (
<section className="rounded-2xl border border-black/10 dark:border-white/10 overflow-hidden bg-white dark:bg-black/20">
<header className="px-4 py-3 border-b border-black/5 dark:border-white/5 flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-semibold">
<ShieldCheck className="w-4 h-4 text-emerald-500" />
{title || "Herramienta Externa"}
</div>
<a
href={launchUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-black/40 dark:text-white/40 hover:text-black/70 dark:hover:text-white/70 inline-flex items-center gap-1"
>
<ExternalLink className="w-3 h-3" />
Abrir en pestaña nueva
</a>
</header>
<iframe
src={launchUrl}
title={title || "Herramienta LTI"}
className="w-full"
style={{ minHeight: "560px", border: "none" }}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
/>
</section>
);
}
+1 -1
View File
@@ -184,7 +184,7 @@ export interface QuizQuestion {
export interface Block {
id: string;
type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'code' | 'hotspot' | 'memory-match' | 'document' | 'audio-response' | 'video_marker' | 'peer-review' | 'role-playing' | 'mermaid' | 'code-lab' | 'scorm' | 'plugin';
type: 'description' | 'media' | 'quiz' | 'fill-in-the-blanks' | 'matching' | 'ordering' | 'short-answer' | 'code' | 'hotspot' | 'memory-match' | 'document' | 'audio-response' | 'video_marker' | 'peer-review' | 'role-playing' | 'mermaid' | 'code-lab' | 'scorm' | 'plugin' | 'lti-tool';
title: string;
content?: string;
url?: string;