таблица lms_content, связь с task

This commit is contained in:
Anton Dzyk 2025-12-14 11:00:20 +03:00
parent ea4bfe281f
commit 3997e2d2ab
23 changed files with 298 additions and 17 deletions

11
Readme.md Normal file
View File

@ -0,0 +1,11 @@
```shell
# Генерация RSA ключа (2048 или 4096 бит)
openssl genrsa -out private.key 2048
# Конвертация в PEM?
# Извлечение публичного ключа
openssl rsa -in private.key -pubout -out public.key
```

View File

@ -2,4 +2,4 @@ spring:
datasource: datasource:
url: jdbc:postgresql://localhost:5432/lti url: jdbc:postgresql://localhost:5432/lti
username: postgres username: postgres
password: postgres password: oodbpasswd

View File

@ -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

15
pom.xml
View File

@ -33,6 +33,11 @@
<artifactId>spring-boot-starter-data-jpa</artifactId> <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
@ -58,10 +63,20 @@
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>

View File

@ -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<Task> tasks = lmsContent.getTasks();
//TODO добавить версию в Task и выбирать самую старшую и опубликованную
Object data = tasks.stream().findFirst().get().getData();
return (String)data;
} else {
return "Not Page";
}
}
}

View File

@ -2,8 +2,10 @@ package ru.oa2.lti;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication @SpringBootApplication
@EnableTransactionManagement
public class LTIProviderApplication { public class LTIProviderApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -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();
}
}

View File

@ -1,7 +1,9 @@
package ru.oa2.lti.controller; package ru.oa2.lti.controller;
import jakarta.servlet.ServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import ru.oa2.lti.ApplicationService;
import ru.oa2.lti.model.LtiLogin; import ru.oa2.lti.model.LtiLogin;
@Slf4j @Slf4j
@ -9,6 +11,12 @@ import ru.oa2.lti.model.LtiLogin;
@RequestMapping("/tool/lti") @RequestMapping("/tool/lti")
public class LoginController { public class LoginController {
private final ApplicationService service;
public LoginController(ApplicationService applicationService) {
this.service = applicationService;
}
@PostMapping("/login") @PostMapping("/login")
public String login( public String login(
@RequestParam("client_id") String clientId, @RequestParam("client_id") String clientId,
@ -16,8 +24,8 @@ public class LoginController {
@RequestParam("login_hint") String loginHint, @RequestParam("login_hint") String loginHint,
@RequestParam("lti_deployment_id") String ltiDeploymentId, @RequestParam("lti_deployment_id") String ltiDeploymentId,
@RequestParam("lti_message_hint") String ltiMessageHint, @RequestParam("lti_message_hint") String ltiMessageHint,
@RequestParam("target_link_uri") String targetLinkUri @RequestParam("target_link_uri") String targetLinkUri,
) { ServletRequest servletRequest) {
var ltiLogin = LtiLogin.builder() var ltiLogin = LtiLogin.builder()
.clientId(clientId) .clientId(clientId)
.iss(iss) .iss(iss)
@ -29,7 +37,6 @@ public class LoginController {
log.info("BODY: {}", ltiLogin); log.info("BODY: {}", ltiLogin);
//TODO service get context (by student/teatcher) return service.getTask(ltiLogin);
return "redirect:" + "/page1";
} }
} }

View File

@ -1,5 +1,5 @@
```shell ```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&lti_deployment_id=2715d045-77c5-4d88-a1e4-92d721fcc54b&lti_message_hint=1802240&target_link_uri=https%3A%2F%2Fopenolat.local%2Ftool' -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&lti_deployment_id=2715d045-77c5-4d88-a1e4-92d721fcc54b&lti_message_hint=1802240&target_link_uri=https%3A%2F%2Fopenolat.local%2Ftool'
``` ```

View File

@ -0,0 +1,5 @@
package ru.oa2.lti.domain.carecase;
public class GetTask {
}

View File

@ -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<LMSContent, Long> {
Optional<LMSContent> getLMSContentByContentKey(long id);
Optional<LMSContent> getLMSContentByContentId(UUID contentId);
Optional<LMSContent> findByContentId(UUID contentId);
}

View File

@ -1,4 +0,0 @@
package ru.oa2.lti.repository;
public interface TaskRepository {
}

View File

@ -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<Task> tasks;
}

View File

@ -1,20 +1,22 @@
package ru.oa2.lti.repository.entities; package ru.oa2.lti.repository.entities;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.Data;
import ru.oa2.lti.model.TaskType; import ru.oa2.lti.model.TaskType;
@Data
@Entity @Entity
@Table(name = "task") @Table(name = "task")
public class Task { public class Task {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
long taskId; long id;
@Column(length = 20) @Column(name = "task_type", length = 20)
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
TaskType taskType; TaskType taskType;
@Column(columnDefinition = "jsonb") @Column(name = "data", columnDefinition = "jsonb")
Object data; Object data;
} }

View File

@ -0,0 +1,6 @@
package ru.oa2.lti.service.jwt;
public interface JwtService {
Payload getPayload(String jwt);
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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.ResultRequest;
import ru.oa2.lti.model.TaskResult; import ru.oa2.lti.model.TaskResult;

View File

@ -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.ResultRequest;
import ru.oa2.lti.model.TaskResult; import ru.oa2.lti.model.TaskResult;

View File

@ -1,5 +1,12 @@
spring: spring:
jpa:
# Показывать SQL запросы
show-sql: true
properties:
hibernate:
format_sql: true
application: application:
name: lti-provider name: lti-provider
@ -12,4 +19,7 @@ spring:
server: server:
port: 9999 port: 9999
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.10.xsd"
objectQuotingStrategy="QUOTE_ONLY_RESERVED_WORDS">
<changeSet id="2025-12-01" author="dzyk">
<createTable tableName="lms_content">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="PK_LMS_CONTENT"/>
</column>
<column name="deployment_id" type="uuid"/>
<column name="content_key" type="bigint"/>
<column name="content_uuid" type="uuid"/>
<column name="task_id" type="bigint"/>
</createTable>
<addForeignKeyConstraint
baseTableName="lms_content"
baseColumnNames="task_id"
constraintName="fk_lms_content_task"
referencedTableName="task"
referencedColumnNames="id"
onDelete="NO ACTION"/>
</changeSet>
</databaseChangeLog>

View File

@ -10,7 +10,7 @@
<createTable tableName="task"> <createTable tableName="task">
<column autoIncrement="true" name="task_id" type="BIGINT"> <column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true" primaryKeyName="PK_TASK"/> <constraints nullable="false" primaryKey="true" primaryKeyName="PK_TASK"/>
</column> </column>

View File

@ -9,5 +9,6 @@
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd"> http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<include file="/db/changelog/1.0.0/2025-12-add_task.xml"/> <include file="/db/changelog/1.0.0/2025-12-add_task.xml"/>
<include file="/db/changelog/1.0.0/2025-12-add_lms_content.xml"/>
</databaseChangeLog> </databaseChangeLog>