From 3997e2d2ab361ba64ee524114af5ee70736bf74e Mon Sep 17 00:00:00 2001 From: Anton Dzyk Date: Sun, 14 Dec 2025 11:00:20 +0300 Subject: [PATCH] =?UTF-8?q?=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B0=20lm?= =?UTF-8?q?s=5Fcontent,=20=D1=81=D0=B2=D1=8F=D0=B7=D1=8C=20=D1=81=20task?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Readme.md | 11 +++++ debug/application.yaml | 2 +- debug/docker-compose.without_pg.yaml | 36 ++++++++++++++++ pom.xml | 15 +++++++ .../java/ru/oa2/lti/ApplicationService.java | 42 +++++++++++++++++++ .../ru/oa2/lti/LTIProviderApplication.java | 2 + .../java/ru/oa2/lti/config/JwtConfig.java | 18 ++++++++ .../oa2/lti/controller/LoginController.java | 15 +++++-- src/main/java/ru/oa2/lti/controller/TODO.md | 2 +- .../ru/oa2/lti/domain/carecase/GetTask.java | 5 +++ .../lti/repository/LMSContentRepository.java | 16 +++++++ .../ru/oa2/lti/repository/TaskRepository.java | 4 -- .../lti/repository/entities/LMSContent.java | 29 +++++++++++++ .../ru/oa2/lti/repository/entities/Task.java | 8 ++-- .../ru/oa2/lti/service/jwt/JwtService.java | 6 +++ .../oa2/lti/service/jwt/JwtServiceImpl.java | 31 ++++++++++++++ .../java/ru/oa2/lti/service/jwt/Payload.java | 18 ++++++++ .../lti/service/{ => task}/TaskService.java | 2 +- .../service/{ => task}/TaskServiceImpl.java | 2 +- src/main/resources/application.yaml | 12 +++++- .../1.0.0/2025-12-add_lms_content.xml | 36 ++++++++++++++++ .../db/changelog/1.0.0/2025-12-add_task.xml | 2 +- .../db/changelog/changelog-master.xml | 1 + 23 files changed, 298 insertions(+), 17 deletions(-) create mode 100644 Readme.md create mode 100644 debug/docker-compose.without_pg.yaml create mode 100644 src/main/java/ru/oa2/lti/ApplicationService.java create mode 100644 src/main/java/ru/oa2/lti/config/JwtConfig.java create mode 100644 src/main/java/ru/oa2/lti/domain/carecase/GetTask.java create mode 100644 src/main/java/ru/oa2/lti/repository/LMSContentRepository.java delete mode 100644 src/main/java/ru/oa2/lti/repository/TaskRepository.java create mode 100644 src/main/java/ru/oa2/lti/repository/entities/LMSContent.java create mode 100644 src/main/java/ru/oa2/lti/service/jwt/JwtService.java create mode 100644 src/main/java/ru/oa2/lti/service/jwt/JwtServiceImpl.java create mode 100644 src/main/java/ru/oa2/lti/service/jwt/Payload.java rename src/main/java/ru/oa2/lti/service/{ => task}/TaskService.java (95%) rename src/main/java/ru/oa2/lti/service/{ => task}/TaskServiceImpl.java (93%) create mode 100644 src/main/resources/db/changelog/1.0.0/2025-12-add_lms_content.xml diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..f5eabdc --- /dev/null +++ b/Readme.md @@ -0,0 +1,11 @@ + +```shell +# Генерация RSA ключа (2048 или 4096 бит) +openssl genrsa -out private.key 2048 + +# Конвертация в PEM? + +# Извлечение публичного ключа +openssl rsa -in private.key -pubout -out public.key + +``` \ No newline at end of file diff --git a/debug/application.yaml b/debug/application.yaml index a056c55..418d09a 100644 --- a/debug/application.yaml +++ b/debug/application.yaml @@ -2,4 +2,4 @@ spring: datasource: url: jdbc:postgresql://localhost:5432/lti username: postgres - password: postgres \ No newline at end of file + password: oodbpasswd \ No newline at end of file diff --git a/debug/docker-compose.without_pg.yaml b/debug/docker-compose.without_pg.yaml new file mode 100644 index 0000000..c95e080 --- /dev/null +++ b/debug/docker-compose.without_pg.yaml @@ -0,0 +1,36 @@ +services: + + zookeeper: + image: confluentinc/cp-zookeeper:7.6.1 + container_name: zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + restart: unless-stopped + + kafka: + image: confluentinc/cp-kafka:7.6.1 + container_name: kafka + depends_on: + - zookeeper + ports: + - "29092:29092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + restart: unless-stopped + +volumes: + postgres_data: + +networks: + lti-net: + driver: bridge + default: + name: lti-net \ No newline at end of file diff --git a/pom.xml b/pom.xml index 127ba4f..6f2a32b 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,11 @@ spring-boot-starter-data-jpa + + org.springframework.security + spring-security-oauth2-jose + + org.projectlombok lombok @@ -58,10 +63,20 @@ + org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + diff --git a/src/main/java/ru/oa2/lti/ApplicationService.java b/src/main/java/ru/oa2/lti/ApplicationService.java new file mode 100644 index 0000000..1397d5f --- /dev/null +++ b/src/main/java/ru/oa2/lti/ApplicationService.java @@ -0,0 +1,42 @@ +package ru.oa2.lti; + +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; +import ru.oa2.lti.model.LtiLogin; +import ru.oa2.lti.repository.LMSContentRepository; +import ru.oa2.lti.repository.entities.LMSContent; +import ru.oa2.lti.repository.entities.Task; +import ru.oa2.lti.service.jwt.JwtService; + +import java.util.Collection; + +@Service +@Transactional +public class ApplicationService { + + private final JwtService jwtService; + private final LMSContentRepository lmsContentRepository; + + public ApplicationService(JwtService jwtService, + LMSContentRepository lmsContentRepository) { + this.jwtService = jwtService; + this.lmsContentRepository = lmsContentRepository; + } + + public String getTask(LtiLogin ltiLogin) { + var payload = jwtService.getPayload(ltiLogin.getLoginHint()); + + var content = lmsContentRepository.getLMSContentByContentId(payload.getContextId()); + + if(content.isPresent()) { + LMSContent lmsContent = content.get(); + Collection tasks = lmsContent.getTasks(); + + //TODO добавить версию в Task и выбирать самую старшую и опубликованную + Object data = tasks.stream().findFirst().get().getData(); + return (String)data; + } else { + return "Not Page"; + } + } +} diff --git a/src/main/java/ru/oa2/lti/LTIProviderApplication.java b/src/main/java/ru/oa2/lti/LTIProviderApplication.java index a06dc27..d23020f 100644 --- a/src/main/java/ru/oa2/lti/LTIProviderApplication.java +++ b/src/main/java/ru/oa2/lti/LTIProviderApplication.java @@ -2,8 +2,10 @@ package ru.oa2.lti; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication +@EnableTransactionManagement public class LTIProviderApplication { public static void main(String[] args) { diff --git a/src/main/java/ru/oa2/lti/config/JwtConfig.java b/src/main/java/ru/oa2/lti/config/JwtConfig.java new file mode 100644 index 0000000..15ca8d9 --- /dev/null +++ b/src/main/java/ru/oa2/lti/config/JwtConfig.java @@ -0,0 +1,18 @@ +package ru.oa2.lti.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + + +@Configuration +public class JwtConfig { + + @Bean + public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder + .withJwkSetUri("http://openolat.local/lti/keys") //TODO адрес из application + .build(); + } +} diff --git a/src/main/java/ru/oa2/lti/controller/LoginController.java b/src/main/java/ru/oa2/lti/controller/LoginController.java index 1f9f44c..98405c6 100644 --- a/src/main/java/ru/oa2/lti/controller/LoginController.java +++ b/src/main/java/ru/oa2/lti/controller/LoginController.java @@ -1,7 +1,9 @@ package ru.oa2.lti.controller; +import jakarta.servlet.ServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +import ru.oa2.lti.ApplicationService; import ru.oa2.lti.model.LtiLogin; @Slf4j @@ -9,6 +11,12 @@ import ru.oa2.lti.model.LtiLogin; @RequestMapping("/tool/lti") public class LoginController { + private final ApplicationService service; + + public LoginController(ApplicationService applicationService) { + this.service = applicationService; + } + @PostMapping("/login") public String login( @RequestParam("client_id") String clientId, @@ -16,8 +24,8 @@ public class LoginController { @RequestParam("login_hint") String loginHint, @RequestParam("lti_deployment_id") String ltiDeploymentId, @RequestParam("lti_message_hint") String ltiMessageHint, - @RequestParam("target_link_uri") String targetLinkUri - ) { + @RequestParam("target_link_uri") String targetLinkUri, + ServletRequest servletRequest) { var ltiLogin = LtiLogin.builder() .clientId(clientId) .iss(iss) @@ -29,7 +37,6 @@ public class LoginController { log.info("BODY: {}", ltiLogin); - //TODO service get context (by student/teatcher) - return "redirect:" + "/page1"; + return service.getTask(ltiLogin); } } diff --git a/src/main/java/ru/oa2/lti/controller/TODO.md b/src/main/java/ru/oa2/lti/controller/TODO.md index ef6474f..ceac1d6 100644 --- a/src/main/java/ru/oa2/lti/controller/TODO.md +++ b/src/main/java/ru/oa2/lti/controller/TODO.md @@ -1,5 +1,5 @@ ```shell -curl -XPOST http://localhost:8080/tool/lti/login \ +curl -XPOST http://localhost:9999/tool/lti/login \ -d 'client_id=27e2d42d-d218-4ab9-b063-85e3ec87ec8f&iss=https%3A%2F%2Flocalhost&login_hint=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImU5ODA3YTg2LTVhNWQtNDI0MC1hYjI1LWNhYTUyZDliZjUxOSJ9.eyJkZXBsb3ltZW50S2V5IjoxLCJkZXBsb3ltZW50SWQiOiIyNzE1ZDA0NS03N2M1LTRkODgtYTFlNC05MmQ3MjFmY2M1NGIiLCJjb250ZXh0S2V5IjoxLCJjb250ZXh0SWQiOiJlMmE4ZGQxMy04MWJhLTQwMTEtODgwNi0wMjA5NGZkZjg5ZjMiLCJjb3Vyc2VhZG1pbiI6ZmFsc2UsImNvYWNoIjpmYWxzZSwicGFydGljaXBhbnQiOnRydWV9.vgPLHQA9scdED3_OwOy46h6VmzpN4arIHY3-YDBdhH4kuEqeCOjjtFbGdDqauKoQBTSVo4UvoK4JQLMhak6qsFchCj54mPob8jbaKLd0GnO_jY0sR609Nrk7Muq7cki_4PjVMX8TTHp-VYlSHjVxQH_z_D5Wld27J95z4qJjRU59GmvLGDqdLyerVVBO-zaavYsUbEEiAxoX3hmytxrarmJ7OHpxufNOeXzZ0DSGUmU5ycuTAqxODaHO1Y4rQM6XlvSfDh_TmXP8QEkatlp2cdjRpNWyOdUW_hZfbtkqukwt1ZP7KEgWzNI3vivpBjm2xfUG2YLwXPJqHa47NQgvsQ<i_deployment_id=2715d045-77c5-4d88-a1e4-92d721fcc54b<i_message_hint=1802240&target_link_uri=https%3A%2F%2Fopenolat.local%2Ftool' ``` \ No newline at end of file diff --git a/src/main/java/ru/oa2/lti/domain/carecase/GetTask.java b/src/main/java/ru/oa2/lti/domain/carecase/GetTask.java new file mode 100644 index 0000000..846d44f --- /dev/null +++ b/src/main/java/ru/oa2/lti/domain/carecase/GetTask.java @@ -0,0 +1,5 @@ +package ru.oa2.lti.domain.carecase; + +public class GetTask { + +} diff --git a/src/main/java/ru/oa2/lti/repository/LMSContentRepository.java b/src/main/java/ru/oa2/lti/repository/LMSContentRepository.java new file mode 100644 index 0000000..4331f13 --- /dev/null +++ b/src/main/java/ru/oa2/lti/repository/LMSContentRepository.java @@ -0,0 +1,16 @@ +package ru.oa2.lti.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.oa2.lti.repository.entities.LMSContent; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface LMSContentRepository extends JpaRepository { + + Optional getLMSContentByContentKey(long id); + Optional getLMSContentByContentId(UUID contentId); + Optional findByContentId(UUID contentId); +} diff --git a/src/main/java/ru/oa2/lti/repository/TaskRepository.java b/src/main/java/ru/oa2/lti/repository/TaskRepository.java deleted file mode 100644 index 4e674af..0000000 --- a/src/main/java/ru/oa2/lti/repository/TaskRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package ru.oa2.lti.repository; - -public interface TaskRepository { -} diff --git a/src/main/java/ru/oa2/lti/repository/entities/LMSContent.java b/src/main/java/ru/oa2/lti/repository/entities/LMSContent.java new file mode 100644 index 0000000..ce8bd84 --- /dev/null +++ b/src/main/java/ru/oa2/lti/repository/entities/LMSContent.java @@ -0,0 +1,29 @@ +package ru.oa2.lti.repository.entities; + +import jakarta.persistence.*; +import lombok.Data; + +import java.util.Collection; +import java.util.UUID; + +@Data +@Entity +@Table(name = "lms_content") +public class LMSContent { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + long id; + + @Column(name = "content_key") + long contentKey; + + @Column(name = "content_uuid") + UUID contentId; + + @Column(name = "deployment_id") + UUID deploymentId; + + @OneToMany(mappedBy="id", fetch = FetchType.LAZY) + Collection tasks; +} diff --git a/src/main/java/ru/oa2/lti/repository/entities/Task.java b/src/main/java/ru/oa2/lti/repository/entities/Task.java index cebbd83..ff214c5 100644 --- a/src/main/java/ru/oa2/lti/repository/entities/Task.java +++ b/src/main/java/ru/oa2/lti/repository/entities/Task.java @@ -1,20 +1,22 @@ package ru.oa2.lti.repository.entities; import jakarta.persistence.*; +import lombok.Data; import ru.oa2.lti.model.TaskType; +@Data @Entity @Table(name = "task") public class Task { @Id @GeneratedValue(strategy = GenerationType.AUTO) - long taskId; + long id; - @Column(length = 20) + @Column(name = "task_type", length = 20) @Enumerated(EnumType.STRING) TaskType taskType; - @Column(columnDefinition = "jsonb") + @Column(name = "data", columnDefinition = "jsonb") Object data; } diff --git a/src/main/java/ru/oa2/lti/service/jwt/JwtService.java b/src/main/java/ru/oa2/lti/service/jwt/JwtService.java new file mode 100644 index 0000000..97e4751 --- /dev/null +++ b/src/main/java/ru/oa2/lti/service/jwt/JwtService.java @@ -0,0 +1,6 @@ +package ru.oa2.lti.service.jwt; + +public interface JwtService { + + Payload getPayload(String jwt); +} diff --git a/src/main/java/ru/oa2/lti/service/jwt/JwtServiceImpl.java b/src/main/java/ru/oa2/lti/service/jwt/JwtServiceImpl.java new file mode 100644 index 0000000..3de1886 --- /dev/null +++ b/src/main/java/ru/oa2/lti/service/jwt/JwtServiceImpl.java @@ -0,0 +1,31 @@ +package ru.oa2.lti.service.jwt; + +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class JwtServiceImpl implements JwtService { + + private JwtDecoder jwtDecoder; + + public JwtServiceImpl(JwtDecoder jwtDecoder) { + this.jwtDecoder = jwtDecoder; + } + + @Override + public Payload getPayload(String token) { + var jwt = jwtDecoder.decode(token); + + return Payload.builder() + .deploymentKey(jwt.getClaim("deploymentKey")) + .deploymentId(UUID.fromString(jwt.getClaim("deploymentId"))) + .contextKey(jwt.getClaim("contextKey")) + .contextId(UUID.fromString(jwt.getClaim("contextId"))) + .courseadmin(jwt.getClaim("courseadmin")) + .coach(jwt.getClaim("coach")) + .participant(jwt.getClaim("participant")) + .build(); + } +} diff --git a/src/main/java/ru/oa2/lti/service/jwt/Payload.java b/src/main/java/ru/oa2/lti/service/jwt/Payload.java new file mode 100644 index 0000000..78cc27c --- /dev/null +++ b/src/main/java/ru/oa2/lti/service/jwt/Payload.java @@ -0,0 +1,18 @@ +package ru.oa2.lti.service.jwt; + +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +public class Payload { + private Long deploymentKey; + private UUID deploymentId; + private Long contextKey; + private UUID contextId; + private Boolean courseadmin; + private Boolean coach; + private Boolean participant; +} diff --git a/src/main/java/ru/oa2/lti/service/TaskService.java b/src/main/java/ru/oa2/lti/service/task/TaskService.java similarity index 95% rename from src/main/java/ru/oa2/lti/service/TaskService.java rename to src/main/java/ru/oa2/lti/service/task/TaskService.java index 3253596..da0b909 100644 --- a/src/main/java/ru/oa2/lti/service/TaskService.java +++ b/src/main/java/ru/oa2/lti/service/task/TaskService.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.service; +package ru.oa2.lti.service.task; import ru.oa2.lti.model.ResultRequest; import ru.oa2.lti.model.TaskResult; diff --git a/src/main/java/ru/oa2/lti/service/TaskServiceImpl.java b/src/main/java/ru/oa2/lti/service/task/TaskServiceImpl.java similarity index 93% rename from src/main/java/ru/oa2/lti/service/TaskServiceImpl.java rename to src/main/java/ru/oa2/lti/service/task/TaskServiceImpl.java index ed0e9f5..c36db20 100644 --- a/src/main/java/ru/oa2/lti/service/TaskServiceImpl.java +++ b/src/main/java/ru/oa2/lti/service/task/TaskServiceImpl.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.service; +package ru.oa2.lti.service.task; import ru.oa2.lti.model.ResultRequest; import ru.oa2.lti.model.TaskResult; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index dec1a39..186e2c1 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,5 +1,12 @@ spring: + jpa: + # Показывать SQL запросы + show-sql: true + properties: + hibernate: + format_sql: true + application: name: lti-provider @@ -12,4 +19,7 @@ spring: server: port: 9999 - +logging: + level: + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE \ No newline at end of file diff --git a/src/main/resources/db/changelog/1.0.0/2025-12-add_lms_content.xml b/src/main/resources/db/changelog/1.0.0/2025-12-add_lms_content.xml new file mode 100644 index 0000000..70b82d7 --- /dev/null +++ b/src/main/resources/db/changelog/1.0.0/2025-12-add_lms_content.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/1.0.0/2025-12-add_task.xml b/src/main/resources/db/changelog/1.0.0/2025-12-add_task.xml index a4f0a7f..efef0d5 100644 --- a/src/main/resources/db/changelog/1.0.0/2025-12-add_task.xml +++ b/src/main/resources/db/changelog/1.0.0/2025-12-add_task.xml @@ -10,7 +10,7 @@ - + diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 6902742..87a6231 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -9,5 +9,6 @@ http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd"> + \ No newline at end of file