refactoring, добавление портов, разделение домена и реализацию
This commit is contained in:
parent
bf68f3fe0a
commit
498ba86305
|
|
@ -5,80 +5,63 @@ import jakarta.servlet.http.HttpSession;
|
|||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.oa2.lti.application.infrastructure.lms.LMSService;
|
||||
import ru.oa2.lti.application.service.history.HistoryService;
|
||||
import ru.oa2.lti.application.service.jwt.JwtService;
|
||||
import ru.oa2.lti.domain.model.auth.LtiLogin;
|
||||
import ru.oa2.lti.application.usecase.lti.HandleLtiRedirectUseCase;
|
||||
import ru.oa2.lti.application.usecase.lti.ProcessLtiLoginUseCase;
|
||||
|
||||
|
||||
/*
|
||||
Входная точка в external tool по протоколу LTI 1.3
|
||||
|
||||
#TODO расписать процесс и добавить линк на доку
|
||||
/**
|
||||
* Входная точка в external tool по протоколу LTI 1.3.
|
||||
*
|
||||
* Контроллер только обрабатывает HTTP запросы и делегирует
|
||||
* бизнес-логику в Use Cases.
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/tool/lti")
|
||||
public class LaunchController {
|
||||
|
||||
private final JwtService jwtService;
|
||||
private final LMSService lmsService;
|
||||
private final HistoryService historyService;
|
||||
private final ProcessLtiLoginUseCase processLtiLoginUseCase;
|
||||
private final HandleLtiRedirectUseCase handleLtiRedirectUseCase;
|
||||
|
||||
public LaunchController(JwtService jwtService,
|
||||
LMSService lmsService,
|
||||
HistoryService historyService) {
|
||||
this.jwtService = jwtService;
|
||||
this.lmsService = lmsService;
|
||||
this.historyService = historyService;
|
||||
public LaunchController(ProcessLtiLoginUseCase processLtiLoginUseCase,
|
||||
HandleLtiRedirectUseCase handleLtiRedirectUseCase) {
|
||||
this.processLtiLoginUseCase = processLtiLoginUseCase;
|
||||
this.handleLtiRedirectUseCase = handleLtiRedirectUseCase;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@PostMapping("/login")
|
||||
public String login(@RequestParam("client_id") String clientId,
|
||||
@RequestParam("iss") String iss,
|
||||
@RequestParam("login_hint") String loginHint,
|
||||
@RequestParam("lti_deployment_id") String ltiDeploymentId,
|
||||
@RequestParam("lti_message_hint") String ltiMessageHint,
|
||||
@RequestParam("target_link_uri") String targetLinkUri,
|
||||
HttpServletRequest request) {
|
||||
@RequestParam("iss") String iss,
|
||||
@RequestParam("login_hint") String loginHint,
|
||||
@RequestParam("lti_deployment_id") String ltiDeploymentId,
|
||||
@RequestParam("lti_message_hint") String ltiMessageHint,
|
||||
@RequestParam("target_link_uri") String targetLinkUri,
|
||||
HttpServletRequest request) {
|
||||
|
||||
var ltiLogin = LtiLogin.builder()
|
||||
.clientId(clientId)
|
||||
.iss(iss)
|
||||
.loginHint(loginHint)
|
||||
.ltiDeploymentId(ltiDeploymentId)
|
||||
.ltiMessageHint(ltiMessageHint)
|
||||
.targetLinkUri(targetLinkUri)
|
||||
.build();
|
||||
// Создаём команду для Use Case
|
||||
var command = new ProcessLtiLoginUseCase.LtiLoginCommand(
|
||||
clientId, iss, loginHint, ltiDeploymentId, ltiMessageHint, targetLinkUri
|
||||
);
|
||||
|
||||
var payload = jwtService.getPayload(ltiLogin.getLoginHint());
|
||||
// Выполняем бизнес-логику через Use Case
|
||||
var result = processLtiLoginUseCase.execute(command);
|
||||
|
||||
// 4. Сохранить access token в сессии
|
||||
HttpSession session = request.getSession();
|
||||
session.setAttribute("lti_login", ltiLogin);
|
||||
session.setAttribute("lti_message_hint", Long.valueOf(ltiMessageHint));
|
||||
session.setAttribute("lti_context_id", payload.getContextId());
|
||||
session.setAttribute("lti_user_id", ltiLogin.getClientId());
|
||||
session.setAttribute("payload", payload);
|
||||
// Сохраняем данные в сессии (HTTP-специфичная логика)
|
||||
HttpSession session = request.getSession();
|
||||
session.setAttribute("lti_login", result.ltiLogin());
|
||||
session.setAttribute("lti_message_hint", Long.valueOf(ltiMessageHint));
|
||||
session.setAttribute("lti_context_id", result.payload().getContextId());
|
||||
session.setAttribute("lti_user_id", clientId);
|
||||
session.setAttribute("payload", result.payload());
|
||||
|
||||
// Логирование события входа
|
||||
historyService.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"LTI_LOGIN",
|
||||
String.format("User login via LTI 1.3, clientId=%s", clientId)
|
||||
);
|
||||
|
||||
if (payload.getContextId() != null) {
|
||||
return lmsService.ltiAuth(ltiMessageHint, iss, loginHint);
|
||||
}
|
||||
|
||||
return "redirect:/tool/lti/select";
|
||||
// Возвращаем результат
|
||||
if (result.requiresSelection()) {
|
||||
return "redirect:/tool/lti/select";
|
||||
}
|
||||
return result.redirectResponse();
|
||||
}
|
||||
|
||||
@GetMapping("/select")
|
||||
public String select(Model model) {
|
||||
//TODO доработать форму выбора существующих заданий
|
||||
return "select";
|
||||
}
|
||||
|
||||
|
|
@ -87,21 +70,14 @@ public class LaunchController {
|
|||
@RequestParam("state") String state,
|
||||
HttpServletRequest request) {
|
||||
|
||||
// Выполняем бизнес-логику через Use Case
|
||||
var result = handleLtiRedirectUseCase.execute(idToken, state);
|
||||
|
||||
// Сохраняем данные в сессии (HTTP-специфичная логика)
|
||||
HttpSession session = request.getSession();
|
||||
session.setAttribute("id_token", idToken);
|
||||
session.setAttribute("state", state);
|
||||
|
||||
// Декодируем id_token для получения информации о контексте
|
||||
var idTokenPayload = jwtService.getTokenPayload(idToken);
|
||||
|
||||
// Логирование перенаправления после авторизации
|
||||
historyService.logAction(
|
||||
idTokenPayload.getDeploymentId(),
|
||||
idTokenPayload.getContext() != null ? idTokenPayload.getContext().id() : null,
|
||||
"LTI_REDIRECT",
|
||||
"User redirected after OAuth authentication"
|
||||
);
|
||||
|
||||
return "redirect:/tool/lti/task";
|
||||
return "redirect:" + result.redirectUrl();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import ru.oa2.lti.application.service.history.HistoryService;
|
||||
import ru.oa2.lti.domain.port.out.HistoryRepository;
|
||||
import ru.oa2.lti.application.service.task.TaskService;
|
||||
import ru.oa2.lti.domain.model.ResultRequest;
|
||||
import ru.oa2.lti.domain.model.auth.LtiLogin;
|
||||
import ru.oa2.lti.domain.model.auth.Payload;
|
||||
import ru.oa2.lti.domain.model.results.ResultResponse;
|
||||
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
|
||||
import ru.oa2.lti.domain.model.task.TaskData;
|
||||
import ru.oa2.lti.application.dto.auth.LtiLogin;
|
||||
import ru.oa2.lti.application.dto.auth.Payload;
|
||||
import ru.oa2.lti.application.dto.result.ResultResponse;
|
||||
import ru.oa2.lti.application.dto.task.CheckTaskRequest;
|
||||
import ru.oa2.lti.application.dto.task.RequestUpdateTask;
|
||||
import ru.oa2.lti.application.dto.task.TaskData;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
|
@ -34,11 +34,11 @@ POST /submit - отправка на автоматическую проверк
|
|||
public class TaskController {
|
||||
|
||||
private final TaskService taskService;
|
||||
private final HistoryService historyService;
|
||||
private final HistoryRepository historyRepository;
|
||||
|
||||
public TaskController(TaskService taskService, HistoryService historyService) {
|
||||
public TaskController(TaskService taskService, HistoryRepository historyRepository) {
|
||||
this.taskService = taskService;
|
||||
this.historyService = historyService;
|
||||
this.historyRepository = historyRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
|
@ -52,7 +52,7 @@ public class TaskController {
|
|||
var data = taskService.getTask(payload.getContextId());
|
||||
|
||||
if (data == null) {
|
||||
historyService.logAction(
|
||||
historyRepository.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_ERROR",
|
||||
|
|
@ -65,7 +65,7 @@ public class TaskController {
|
|||
model.addAttribute("description", data.getDescription());
|
||||
|
||||
// Логирование просмотра задачи
|
||||
historyService.logAction(
|
||||
historyRepository.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_VIEW",
|
||||
|
|
@ -93,7 +93,7 @@ public class TaskController {
|
|||
|
||||
if (payload != null && payload.getCoach()) {
|
||||
// Логирование обновления задачи
|
||||
historyService.logAction(
|
||||
historyRepository.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_UPDATE",
|
||||
|
|
@ -110,7 +110,7 @@ public class TaskController {
|
|||
|
||||
// Логирование попытки несанкционированного обновления
|
||||
if (payload != null) {
|
||||
historyService.logAction(
|
||||
historyRepository.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_UPDATE_DENIED",
|
||||
|
|
@ -134,7 +134,7 @@ public class TaskController {
|
|||
|
||||
// Логирование отправки задачи на проверку
|
||||
if (payload != null) {
|
||||
historyService.logAction(
|
||||
historyRepository.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_SUBMIT",
|
||||
|
|
@ -144,6 +144,6 @@ public class TaskController {
|
|||
|
||||
return ResponseEntity
|
||||
.accepted()
|
||||
.body(taskService.checkTask(new ResultRequest(ltiLogin.getClientId(), idToken)));
|
||||
.body(taskService.checkTask(new CheckTaskRequest(ltiLogin.getClientId(), idToken)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
package ru.oa2.lti.application.domain.carecase;
|
||||
|
||||
public class GetTask {
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
/*
|
||||
"https://purl.imsglobal.org/spec/lti/claim/launch_presentation" : {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.auth;
|
||||
package ru.oa2.lti.application.dto.auth;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.results;
|
||||
package ru.oa2.lti.application.dto.result;
|
||||
|
||||
public enum GradingType {
|
||||
NotStarted,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.results;
|
||||
package ru.oa2.lti.application.dto.result;
|
||||
|
||||
public record Lineitems(
|
||||
String id,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.results;
|
||||
package ru.oa2.lti.application.dto.result;
|
||||
|
||||
public enum ProgressType {
|
||||
Initialized,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.results;
|
||||
package ru.oa2.lti.application.dto.result;
|
||||
|
||||
|
||||
import lombok.Builder;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.results;
|
||||
package ru.oa2.lti.application.dto.result;
|
||||
|
||||
public record ResultResponse(
|
||||
String status
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.oa2.lti.application.dto.task;
|
||||
|
||||
/**
|
||||
* DTO для запроса проверки задачи
|
||||
*/
|
||||
public record CheckTaskRequest(
|
||||
String clientId,
|
||||
String idToken
|
||||
) {}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.task;
|
||||
package ru.oa2.lti.application.dto.task;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.task;
|
||||
package ru.oa2.lti.application.dto.task;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.task;
|
||||
package ru.oa2.lti.application.dto.task;
|
||||
|
||||
public enum TaskQueueStatus {
|
||||
NEW,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.task;
|
||||
package ru.oa2.lti.application.dto.task;
|
||||
|
||||
public record TaskResult(
|
||||
//TODO результат работы(лог), true/false, error:<msg>, оценка
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.model.task;
|
||||
package ru.oa2.lti.application.dto.task;
|
||||
|
||||
public enum TaskType {
|
||||
DOCKER,
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package ru.oa2.lti.application.infrastructure.lms;
|
||||
|
||||
public interface LMSService {
|
||||
|
||||
String ltiAuth(String ltiLoginHint, String iss, String loginHint);
|
||||
|
||||
// Вызывается после LTI-запуска
|
||||
String exchangeForAccessToken(String clientId);
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package ru.oa2.lti.application.infrastructure.runner;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface Runner {
|
||||
|
||||
boolean run(UUID userId, String script);
|
||||
|
||||
boolean check(UUID userId, String script);
|
||||
|
||||
boolean delete(UUID userId, String script);
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
package ru.oa2.lti.application.infrastructure.runner;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.oa2.lti.application.service.history.HistoryService;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RunnerImpl implements Runner {
|
||||
|
||||
private final String TEMP_DIR = "./temp/";
|
||||
private final HistoryService historyService;
|
||||
|
||||
public RunnerImpl(HistoryService historyService) {
|
||||
this.historyService = historyService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean run(UUID userId, String script) {
|
||||
try {
|
||||
historyService.logAction(null, userId, "SCRIPT_RUN_START", "Init script execution started");
|
||||
boolean result = runScript(userId, script);
|
||||
|
||||
if (result) {
|
||||
historyService.logAction(null, userId, "SCRIPT_RUN_SUCCESS", "Init script completed successfully");
|
||||
} else {
|
||||
historyService.logAction(null, userId, "SCRIPT_RUN_FAILED", "Init script failed with non-zero exit code");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.error(ex.getMessage());
|
||||
historyService.logAction(null, userId, "SCRIPT_RUN_ERROR", "Init script error: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO добавить вывод результата/интерпретации
|
||||
@Override
|
||||
public boolean check(UUID userId, String script) {
|
||||
try {
|
||||
historyService.logAction(null, userId, "SCRIPT_CHECK_START", "Verification script execution started");
|
||||
boolean result = runScript(userId, script);
|
||||
|
||||
if (result) {
|
||||
historyService.logAction(null, userId, "SCRIPT_CHECK_SUCCESS", "Verification script completed successfully");
|
||||
} else {
|
||||
historyService.logAction(null, userId, "SCRIPT_CHECK_FAILED", "Verification script failed");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.error(ex.getMessage());
|
||||
historyService.logAction(null, userId, "SCRIPT_CHECK_ERROR", "Verification script error: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(UUID userId, String script) {
|
||||
//TODO реализовать метод удаления сущностей
|
||||
historyService.logAction(null, userId, "SCRIPT_DELETE_CALLED", "Delete script called (not implemented)");
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean runScript(UUID userId, String script) throws Exception {
|
||||
|
||||
var processBuilder = new ProcessBuilder();
|
||||
processBuilder.command("bash", saveFile(userId, script));
|
||||
|
||||
var process = processBuilder.start();
|
||||
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream())
|
||||
);
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.append(line).append("\n");
|
||||
}
|
||||
log.info(output.toString());
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
return exitCode == 0;
|
||||
}
|
||||
|
||||
private String saveFile(UUID userId, String script) throws IOException {
|
||||
Path path = Paths.get(TEMP_DIR + userId.toString());
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
Files.createDirectories(path);
|
||||
}
|
||||
var tm = System.currentTimeMillis();
|
||||
var scriptPath = Files.write(
|
||||
Paths.get(TEMP_DIR + userId + "/" + tm),
|
||||
script.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return scriptPath.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package ru.oa2.lti.application.service.history;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HistoryService {
|
||||
|
||||
/**
|
||||
* Логирование события
|
||||
*
|
||||
* @param deploymentId ID деплоймента LMS
|
||||
* @param contentUuid UUID контента
|
||||
* @param message Сообщение о событии
|
||||
*/
|
||||
void logEvent(UUID deploymentId, UUID contentUuid, String message);
|
||||
|
||||
/**
|
||||
* Логирование события с автоматической меткой времени
|
||||
*
|
||||
* @param deploymentId ID деплоймента LMS
|
||||
* @param contentUuid UUID контента
|
||||
* @param action Действие (например, "LOGIN", "TASK_LOADED")
|
||||
* @param details Детали события
|
||||
*/
|
||||
void logAction(UUID deploymentId, UUID contentUuid, String action, String details);
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package ru.oa2.lti.application.service.history;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.HistoryJpaRepository;
|
||||
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class HistoryServiceImpl implements HistoryService {
|
||||
|
||||
private final HistoryJpaRepository historyRepository;
|
||||
|
||||
public HistoryServiceImpl(HistoryJpaRepository historyRepository) {
|
||||
this.historyRepository = historyRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void logEvent(UUID deploymentId, UUID contentUuid, String message) {
|
||||
try {
|
||||
HistoryEntity history = new HistoryEntity();
|
||||
history.setDeploymentId(deploymentId);
|
||||
history.setContentUuid(contentUuid);
|
||||
history.setCreated(LocalDateTime.now());
|
||||
history.setMessage(truncateMessage(message));
|
||||
|
||||
historyRepository.save(history);
|
||||
log.debug("Event logged: deploymentId={}, contentUuid={}, message={}",
|
||||
deploymentId, contentUuid, message);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to log event: deploymentId={}, contentUuid={}, message={}",
|
||||
deploymentId, contentUuid, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void logAction(UUID deploymentId, UUID contentUuid, String action, String details) {
|
||||
String message = String.format("[%s] %s", action, details);
|
||||
logEvent(deploymentId, contentUuid, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрезка сообщения до максимальной длины (2048 символов)
|
||||
*/
|
||||
private String truncateMessage(String message) {
|
||||
if (message == null) {
|
||||
return "";
|
||||
}
|
||||
if (message.length() > 2048) {
|
||||
return message.substring(0, 2045) + "...";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package ru.oa2.lti.application.service.jwt;
|
||||
|
||||
import ru.oa2.lti.domain.model.auth.IdTokenPayload;
|
||||
import ru.oa2.lti.domain.model.auth.Payload;
|
||||
import ru.oa2.lti.application.dto.auth.IdTokenPayload;
|
||||
import ru.oa2.lti.application.dto.auth.Payload;
|
||||
|
||||
public interface JwtService {
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package ru.oa2.lti.application.service.jwt;
|
|||
import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.oa2.lti.domain.model.auth.*;
|
||||
import ru.oa2.lti.application.dto.auth.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import ru.oa2.lti.application.service.jwt.JwtService;
|
||||
import ru.oa2.lti.domain.model.results.GradingType;
|
||||
import ru.oa2.lti.domain.model.results.ProgressType;
|
||||
import ru.oa2.lti.domain.model.results.ResultRequest;
|
||||
import ru.oa2.lti.application.dto.result.GradingType;
|
||||
import ru.oa2.lti.application.dto.result.ProgressType;
|
||||
import ru.oa2.lti.application.dto.result.ResultRequest;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package ru.oa2.lti.application.service.task;
|
||||
|
||||
import ru.oa2.lti.domain.model.ResultRequest;
|
||||
import ru.oa2.lti.domain.model.results.ResultResponse;
|
||||
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
|
||||
import ru.oa2.lti.domain.model.task.TaskData;
|
||||
import ru.oa2.lti.application.dto.result.ResultResponse;
|
||||
import ru.oa2.lti.application.dto.task.CheckTaskRequest;
|
||||
import ru.oa2.lti.application.dto.task.RequestUpdateTask;
|
||||
import ru.oa2.lti.application.dto.task.TaskData;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ public interface TaskService {
|
|||
/*
|
||||
Запуск проверки через скрипт
|
||||
*/
|
||||
ResultResponse checkTask(ResultRequest resultRequest);
|
||||
ResultResponse checkTask(CheckTaskRequest request);
|
||||
|
||||
/*
|
||||
Сохранение/обновление task-а
|
||||
|
|
|
|||
|
|
@ -4,31 +4,29 @@ import lombok.RequiredArgsConstructor;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.oa2.lti.application.infrastructure.lms.LMSService;
|
||||
import ru.oa2.lti.application.infrastructure.runner.Runner;
|
||||
import ru.oa2.lti.application.service.history.HistoryService;
|
||||
import ru.oa2.lti.application.service.results.ResultService;
|
||||
import ru.oa2.lti.domain.exception.TaskNotFoundException;
|
||||
import ru.oa2.lti.domain.model.Task;
|
||||
import ru.oa2.lti.domain.repository.TaskRepository;
|
||||
import ru.oa2.lti.domain.port.out.HistoryRepository;
|
||||
import ru.oa2.lti.domain.port.out.LMSGateway;
|
||||
import ru.oa2.lti.domain.port.out.ScriptExecutor;
|
||||
import ru.oa2.lti.domain.port.out.TaskRepository;
|
||||
import ru.oa2.lti.domain.valueobject.*;
|
||||
import ru.oa2.lti.domain.model.ResultRequest;
|
||||
import ru.oa2.lti.domain.model.results.ResultResponse;
|
||||
import ru.oa2.lti.domain.model.task.RequestUpdateTask;
|
||||
import ru.oa2.lti.domain.model.task.TaskData;
|
||||
import ru.oa2.lti.application.dto.result.ResultResponse;
|
||||
import ru.oa2.lti.application.dto.task.CheckTaskRequest;
|
||||
import ru.oa2.lti.application.dto.task.RequestUpdateTask;
|
||||
import ru.oa2.lti.application.dto.task.TaskData;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Реализация TaskService, следующая принципам Clean Architecture и DDD.
|
||||
*
|
||||
* Улучшения по сравнению со старой версией:
|
||||
* Улучшения:
|
||||
* - Использует доменную модель Task вместо JPA Entity
|
||||
* - Зависит от порта TaskRepository (domain), а не от конкретной реализации
|
||||
* - Бизнес-логика теперь в доменной модели Task
|
||||
* - Зависит от портов (domain), а не от конкретных реализаций
|
||||
* - Бизнес-логика в доменной модели Task
|
||||
* - Сервис только координирует взаимодействие между компонентами
|
||||
* - Явная обработка исключений
|
||||
* - Транзакции только на методах записи
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
|
|
@ -36,27 +34,24 @@ import java.util.UUID;
|
|||
public class TaskServiceImpl implements TaskService {
|
||||
|
||||
private final TaskRepository taskRepository;
|
||||
private final Runner runner;
|
||||
private final HistoryService historyService;
|
||||
private final ScriptExecutor scriptExecutor;
|
||||
private final HistoryRepository historyRepository;
|
||||
private final ResultService resultService;
|
||||
private final LMSService lmsService;
|
||||
private final LMSGateway lmsGateway;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public TaskData getTask(UUID contextId) {
|
||||
log.info("Getting task for context: {}", contextId);
|
||||
|
||||
// Используем доменный репозиторий через порт
|
||||
Task task = taskRepository.findByContextId(contextId)
|
||||
.orElseThrow(() -> new TaskNotFoundException(contextId));
|
||||
|
||||
// Преобразуем в DTO для presentation layer
|
||||
TaskData data = taskToDto(task);
|
||||
|
||||
// Выполняем инициализационный скрипт
|
||||
Script initScript = task.getInitScript();
|
||||
if (initScript.isExecutable()) {
|
||||
boolean success = runner.run(contextId, initScript.content());
|
||||
boolean success = scriptExecutor.runInitScript(contextId, initScript.content());
|
||||
if (!success) {
|
||||
log.error("Init script failed for context: {}", contextId);
|
||||
throw new RuntimeException("Initialization script execution failed");
|
||||
|
|
@ -68,16 +63,12 @@ public class TaskServiceImpl implements TaskService {
|
|||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResultResponse checkTask(ResultRequest resultRequest) {
|
||||
log.info("Checking task for clientId: {}", resultRequest.clientId());
|
||||
public ResultResponse checkTask(CheckTaskRequest request) {
|
||||
log.info("Checking task for clientId: {}", request.clientId());
|
||||
|
||||
// Получаем access token от LMS
|
||||
String accessToken = lmsService.exchangeForAccessToken(resultRequest.clientId());
|
||||
String accessToken = lmsGateway.exchangeForAccessToken(request.clientId());
|
||||
resultService.setResult(accessToken, request.idToken());
|
||||
|
||||
// Отправляем результат
|
||||
resultService.setResult(accessToken, resultRequest.idToken());
|
||||
|
||||
// TODO: реализовать запуск скрипта проверки
|
||||
return new ResultResponse("success");
|
||||
}
|
||||
|
||||
|
|
@ -87,21 +78,16 @@ public class TaskServiceImpl implements TaskService {
|
|||
log.info("Saving task for context: {}", requestUpdateTask.getContextId());
|
||||
|
||||
try {
|
||||
// Загружаем существующую задачу
|
||||
Task task = taskRepository.findByContextId(requestUpdateTask.getContextId())
|
||||
.orElseThrow(() -> new TaskNotFoundException(requestUpdateTask.getContextId()));
|
||||
|
||||
String oldName = task.getName().value();
|
||||
|
||||
// Обновляем задачу через доменную модель (бизнес-логика внутри)
|
||||
updateTaskFromDto(task, requestUpdateTask.getData());
|
||||
|
||||
// Сохраняем через порт репозитория
|
||||
taskRepository.save(task);
|
||||
|
||||
// Логируем действие
|
||||
historyService.logAction(
|
||||
null, // deploymentId нужно получать из контекста
|
||||
historyRepository.logAction(
|
||||
null,
|
||||
requestUpdateTask.getContextId(),
|
||||
"TASK_SAVED",
|
||||
String.format("Task saved: %s (was: %s)",
|
||||
|
|
@ -111,7 +97,7 @@ public class TaskServiceImpl implements TaskService {
|
|||
return new ResultResponse("success");
|
||||
} catch (TaskNotFoundException e) {
|
||||
log.error("Task not found for context: {}", requestUpdateTask.getContextId(), e);
|
||||
historyService.logAction(
|
||||
historyRepository.logAction(
|
||||
null,
|
||||
requestUpdateTask.getContextId(),
|
||||
"TASK_SAVE_ERROR",
|
||||
|
|
@ -120,7 +106,7 @@ public class TaskServiceImpl implements TaskService {
|
|||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("Error saving task", e);
|
||||
historyService.logAction(
|
||||
historyRepository.logAction(
|
||||
null,
|
||||
requestUpdateTask.getContextId(),
|
||||
"TASK_SAVE_ERROR",
|
||||
|
|
@ -130,9 +116,6 @@ public class TaskServiceImpl implements TaskService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует доменную модель Task в DTO TaskData
|
||||
*/
|
||||
private TaskData taskToDto(Task task) {
|
||||
return new TaskData(
|
||||
task.getName().value(),
|
||||
|
|
@ -143,17 +126,12 @@ public class TaskServiceImpl implements TaskService {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет доменную модель Task из DTO TaskData
|
||||
*/
|
||||
private void updateTaskFromDto(Task task, TaskData data) {
|
||||
// Используем Value Objects с валидацией
|
||||
task.update(
|
||||
TaskName.of(data.getName()),
|
||||
Description.of(data.getDescription())
|
||||
);
|
||||
|
||||
// Обновляем скрипты
|
||||
if (data.getInitScript() != null) {
|
||||
task.setScript(Script.of(ScriptType.INITIALIZATION, data.getInitScript()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package ru.oa2.lti.application.usecase.lti;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.oa2.lti.application.dto.auth.IdTokenPayload;
|
||||
import ru.oa2.lti.application.service.jwt.JwtService;
|
||||
import ru.oa2.lti.domain.port.out.HistoryRepository;
|
||||
|
||||
/**
|
||||
* Use Case для обработки редиректа после LTI аутентификации.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HandleLtiRedirectUseCase {
|
||||
|
||||
private final JwtService jwtService;
|
||||
private final HistoryRepository historyRepository;
|
||||
|
||||
/**
|
||||
* Результат обработки редиректа
|
||||
*/
|
||||
public record LtiRedirectResult(
|
||||
IdTokenPayload idTokenPayload,
|
||||
String redirectUrl
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Обрабатывает редирект после OAuth аутентификации
|
||||
*/
|
||||
public LtiRedirectResult execute(String idToken, String state) {
|
||||
log.info("Handling LTI redirect");
|
||||
|
||||
// Декодируем id_token
|
||||
var idTokenPayload = jwtService.getTokenPayload(idToken);
|
||||
|
||||
// Логируем перенаправление
|
||||
historyRepository.logAction(
|
||||
idTokenPayload.getDeploymentId(),
|
||||
idTokenPayload.getContext() != null ? idTokenPayload.getContext().id() : null,
|
||||
"LTI_REDIRECT",
|
||||
"User redirected after OAuth authentication"
|
||||
);
|
||||
|
||||
return new LtiRedirectResult(idTokenPayload, "/tool/lti/task");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package ru.oa2.lti.application.usecase.lti;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.oa2.lti.application.dto.auth.LtiLogin;
|
||||
import ru.oa2.lti.application.dto.auth.Payload;
|
||||
import ru.oa2.lti.application.service.jwt.JwtService;
|
||||
import ru.oa2.lti.domain.port.out.HistoryRepository;
|
||||
import ru.oa2.lti.domain.port.out.LMSGateway;
|
||||
|
||||
/**
|
||||
* Use Case для обработки LTI логина.
|
||||
* Инкапсулирует бизнес-логику аутентификации через LTI 1.3.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProcessLtiLoginUseCase {
|
||||
|
||||
private final JwtService jwtService;
|
||||
private final LMSGateway lmsGateway;
|
||||
private final HistoryRepository historyRepository;
|
||||
|
||||
/**
|
||||
* Результат обработки LTI логина
|
||||
*/
|
||||
public record LtiLoginResult(
|
||||
LtiLogin ltiLogin,
|
||||
Payload payload,
|
||||
String redirectResponse,
|
||||
boolean requiresSelection
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Команда для обработки LTI логина
|
||||
*/
|
||||
public record LtiLoginCommand(
|
||||
String clientId,
|
||||
String iss,
|
||||
String loginHint,
|
||||
String ltiDeploymentId,
|
||||
String ltiMessageHint,
|
||||
String targetLinkUri
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Обрабатывает LTI логин и возвращает результат
|
||||
*/
|
||||
public LtiLoginResult execute(LtiLoginCommand command) {
|
||||
log.info("Processing LTI login for clientId: {}", command.clientId());
|
||||
|
||||
// Создаём объект LtiLogin
|
||||
var ltiLogin = LtiLogin.builder()
|
||||
.clientId(command.clientId())
|
||||
.iss(command.iss())
|
||||
.loginHint(command.loginHint())
|
||||
.ltiDeploymentId(command.ltiDeploymentId())
|
||||
.ltiMessageHint(command.ltiMessageHint())
|
||||
.targetLinkUri(command.targetLinkUri())
|
||||
.build();
|
||||
|
||||
// Получаем payload из JWT
|
||||
var payload = jwtService.getPayload(ltiLogin.getLoginHint());
|
||||
|
||||
// Логируем событие входа
|
||||
historyRepository.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"LTI_LOGIN",
|
||||
String.format("User login via LTI 1.3, clientId=%s", command.clientId())
|
||||
);
|
||||
|
||||
// Проверяем, есть ли contextId
|
||||
if (payload.getContextId() != null) {
|
||||
// Выполняем аутентификацию в LMS
|
||||
String redirectResponse = lmsGateway.ltiAuth(
|
||||
command.ltiMessageHint(),
|
||||
command.iss(),
|
||||
command.loginHint()
|
||||
);
|
||||
return new LtiLoginResult(ltiLogin, payload, redirectResponse, false);
|
||||
}
|
||||
|
||||
// Требуется выбор задания
|
||||
return new LtiLoginResult(ltiLogin, payload, null, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package ru.oa2.lti.domain.model;
|
||||
|
||||
//TODO 2 одинаковых имени? )
|
||||
public record ResultRequest(
|
||||
String clientId,
|
||||
String idToken
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ru.oa2.lti.domain.port.out;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Выходной порт для записи истории событий.
|
||||
* Определяет контракт для логирования действий в системе.
|
||||
*
|
||||
* Принципы:
|
||||
* - Hexagonal Architecture: это выходной порт (driven port)
|
||||
* - Domain слой определяет интерфейс, Infrastructure реализует
|
||||
*/
|
||||
public interface HistoryRepository {
|
||||
|
||||
/**
|
||||
* Записывает событие в историю
|
||||
* @param deploymentId идентификатор развертывания
|
||||
* @param contentUuid UUID контента
|
||||
* @param message сообщение события
|
||||
*/
|
||||
void logEvent(UUID deploymentId, UUID contentUuid, String message);
|
||||
|
||||
/**
|
||||
* Записывает действие с типом в историю
|
||||
* @param deploymentId идентификатор развертывания
|
||||
* @param contentUuid UUID контента
|
||||
* @param actionType тип действия
|
||||
* @param details детали действия
|
||||
*/
|
||||
void logAction(UUID deploymentId, UUID contentUuid, String actionType, String details);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package ru.oa2.lti.domain.port.out;
|
||||
|
||||
/**
|
||||
* Выходной порт для интеграции с LMS (Learning Management System).
|
||||
* Определяет контракт для взаимодействия с внешней LMS системой.
|
||||
*
|
||||
* Принципы:
|
||||
* - Hexagonal Architecture: это выходной порт (driven port)
|
||||
* - Domain слой определяет интерфейс, Infrastructure реализует
|
||||
*/
|
||||
public interface LMSGateway {
|
||||
|
||||
/**
|
||||
* Выполняет LTI аутентификацию
|
||||
* @param ltiLoginHint подсказка для входа LTI
|
||||
* @param iss издатель токена
|
||||
* @param loginHint подсказка для входа
|
||||
* @return URL для редиректа или HTML ответ
|
||||
*/
|
||||
String ltiAuth(String ltiLoginHint, String iss, String loginHint);
|
||||
|
||||
/**
|
||||
* Обменивает учетные данные на access token для работы с LMS API
|
||||
* @param clientId идентификатор клиента
|
||||
* @return access token
|
||||
*/
|
||||
String exchangeForAccessToken(String clientId);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package ru.oa2.lti.domain.port.out;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Выходной порт для выполнения скриптов.
|
||||
* Определяет контракт для запуска, проверки и удаления скриптов.
|
||||
*
|
||||
* Принципы:
|
||||
* - Hexagonal Architecture: это выходной порт (driven port)
|
||||
* - Domain слой определяет интерфейс, Infrastructure реализует
|
||||
*/
|
||||
public interface ScriptExecutor {
|
||||
|
||||
/**
|
||||
* Запускает инициализационный скрипт для пользователя
|
||||
* @param contextId идентификатор контекста
|
||||
* @param script содержимое скрипта
|
||||
* @return true если скрипт выполнен успешно
|
||||
*/
|
||||
boolean runInitScript(UUID contextId, String script);
|
||||
|
||||
/**
|
||||
* Запускает скрипт проверки для пользователя
|
||||
* @param contextId идентификатор контекста
|
||||
* @param script содержимое скрипта
|
||||
* @return true если проверка прошла успешно
|
||||
*/
|
||||
boolean runVerificationScript(UUID contextId, String script);
|
||||
|
||||
/**
|
||||
* Запускает скрипт удаления/очистки для пользователя
|
||||
* @param contextId идентификатор контекста
|
||||
* @param script содержимое скрипта
|
||||
* @return true если очистка прошла успешно
|
||||
*/
|
||||
boolean runCleanupScript(UUID contextId, String script);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.domain.repository;
|
||||
package ru.oa2.lti.domain.port.out;
|
||||
|
||||
import ru.oa2.lti.domain.model.Task;
|
||||
import ru.oa2.lti.domain.valueobject.TaskId;
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package ru.oa2.lti.infrastructure.adapter.history;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.oa2.lti.domain.port.out.HistoryRepository;
|
||||
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.HistoryEntity;
|
||||
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.repository.HistoryJpaRepository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* JPA адаптер для записи истории событий.
|
||||
* Реализует выходной порт HistoryRepository.
|
||||
*
|
||||
* Принципы:
|
||||
* - Hexagonal Architecture: это исходящий адаптер (driven adapter)
|
||||
* - Инкапсулирует детали работы с JPA
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HistoryJpaAdapter implements HistoryRepository {
|
||||
|
||||
private static final int MAX_MESSAGE_LENGTH = 2048;
|
||||
private final HistoryJpaRepository jpaRepository;
|
||||
|
||||
public HistoryJpaAdapter(HistoryJpaRepository jpaRepository) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void logEvent(UUID deploymentId, UUID contentUuid, String message) {
|
||||
try {
|
||||
HistoryEntity history = new HistoryEntity();
|
||||
history.setDeploymentId(deploymentId);
|
||||
history.setContentUuid(contentUuid);
|
||||
history.setCreated(LocalDateTime.now());
|
||||
history.setMessage(truncateMessage(message));
|
||||
|
||||
jpaRepository.save(history);
|
||||
log.debug("Event logged: deploymentId={}, contentUuid={}, message={}",
|
||||
deploymentId, contentUuid, message);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to log event: deploymentId={}, contentUuid={}, message={}",
|
||||
deploymentId, contentUuid, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void logAction(UUID deploymentId, UUID contentUuid, String actionType, String details) {
|
||||
try {
|
||||
HistoryEntity history = new HistoryEntity();
|
||||
history.setDeploymentId(deploymentId);
|
||||
history.setContentUuid(contentUuid);
|
||||
history.setCreated(LocalDateTime.now());
|
||||
history.setActionType(actionType);
|
||||
history.setMessage(truncateMessage(details));
|
||||
|
||||
jpaRepository.save(history);
|
||||
log.debug("Action logged: deploymentId={}, contentUuid={}, action={}, details={}",
|
||||
deploymentId, contentUuid, actionType, details);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to log action: deploymentId={}, contentUuid={}, action={}",
|
||||
deploymentId, contentUuid, actionType, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String truncateMessage(String message) {
|
||||
if (message == null) {
|
||||
return "";
|
||||
}
|
||||
if (message.length() > MAX_MESSAGE_LENGTH) {
|
||||
return message.substring(0, MAX_MESSAGE_LENGTH - 3) + "...";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package ru.oa2.lti.application.infrastructure.lms;
|
||||
package ru.oa2.lti.infrastructure.adapter.lms;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
|
|
@ -8,19 +8,27 @@ import org.springframework.util.MultiValueMap;
|
|||
import org.springframework.web.client.RestClient;
|
||||
import ru.oa2.lti.application.config.AppProperties;
|
||||
import ru.oa2.lti.application.service.jwt.JwtAssertionGenerator;
|
||||
import ru.oa2.lti.domain.model.auth.Scope;
|
||||
import ru.oa2.lti.application.dto.auth.Scope;
|
||||
import ru.oa2.lti.domain.port.out.LMSGateway;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Адаптер для интеграции с LMS через REST API.
|
||||
* Реализует выходной порт LMSGateway.
|
||||
*
|
||||
* Принципы:
|
||||
* - Hexagonal Architecture: это исходящий адаптер (driven adapter)
|
||||
* - Инкапсулирует детали HTTP взаимодействия с LMS
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LMSServiceImpl implements LMSService {
|
||||
public class LMSRestAdapter implements LMSGateway {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final AppProperties appProperties;
|
||||
|
||||
public LMSServiceImpl(RestClient restClient, AppProperties appProperties) {
|
||||
public LMSRestAdapter(RestClient restClient, AppProperties appProperties) {
|
||||
this.restClient = restClient;
|
||||
this.appProperties = appProperties;
|
||||
}
|
||||
|
|
@ -30,8 +38,8 @@ public class LMSServiceImpl implements LMSService {
|
|||
var result = restClient
|
||||
.get()
|
||||
.uri("/lti/auth?" +
|
||||
"state=start" + //TODO а какие могут быть статусы? нужно в Enum перенести
|
||||
"&code=code1" + //TODO что за коды?
|
||||
"state=start" +
|
||||
"&code=code1" +
|
||||
"&iss=" + iss +
|
||||
"&login_hint=" + loginHint +
|
||||
"&redirect_uri=" + appProperties.lmsUrl() + "/tool/lti/redirect" +
|
||||
|
|
@ -47,11 +55,9 @@ public class LMSServiceImpl implements LMSService {
|
|||
|
||||
@Override
|
||||
public String exchangeForAccessToken(String clientId) {
|
||||
|
||||
var endpointUrl = appProperties.lmsUrl() + "/lti/token";
|
||||
|
||||
try {
|
||||
|
||||
var clientAssertion = getClientAssertion(clientId, endpointUrl);
|
||||
var body = getBody(clientAssertion);
|
||||
|
||||
|
|
@ -66,34 +72,31 @@ public class LMSServiceImpl implements LMSService {
|
|||
if (responseBody != null && responseBody.containsKey("access_token")) {
|
||||
return (String) responseBody.get("access_token");
|
||||
} else {
|
||||
throw new RuntimeException("Ответ не содержит access_token");
|
||||
throw new RuntimeException("Response does not contain access_token");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Ошибка получения токена: " + e.getMessage(), e);
|
||||
throw new RuntimeException("Error getting token: " + e.getMessage(), e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Не удалось получить access token", e);
|
||||
throw new RuntimeException("Failed to get access token", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getClientAssertion(String clientId, String endpointUrl) throws Exception {
|
||||
// Генерируем client_assertion
|
||||
return JwtAssertionGenerator.generateClientAssertion(
|
||||
clientId,
|
||||
endpointUrl,
|
||||
"my-key-id-1" //TODO должен совпадать с тем, что в JWKS
|
||||
"my-key-id-1"
|
||||
);
|
||||
}
|
||||
|
||||
private MultiValueMap<String, String> getBody(String clientAssertion) {
|
||||
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
body.add("grant_type", "client_credentials");
|
||||
body.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
|
||||
body.add("client_assertion", clientAssertion);
|
||||
body.add("scope", Scope.scope(Scope.LINE_ITEM, Scope.SCOPE, Scope.ENDPOINT));
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package ru.oa2.lti.infrastructure.adapter.persistence.jpa;
|
|||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.oa2.lti.domain.model.Task;
|
||||
import ru.oa2.lti.domain.repository.TaskRepository;
|
||||
import ru.oa2.lti.domain.port.out.TaskRepository;
|
||||
import ru.oa2.lti.domain.valueobject.TaskId;
|
||||
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.entity.TaskEntity;
|
||||
import ru.oa2.lti.infrastructure.adapter.persistence.jpa.mapper.TaskEntityMapper;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
package ru.oa2.lti.infrastructure.adapter.script;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.oa2.lti.domain.port.out.HistoryRepository;
|
||||
import ru.oa2.lti.domain.port.out.ScriptExecutor;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Адаптер для выполнения Bash скриптов.
|
||||
* Реализует выходной порт ScriptExecutor.
|
||||
*
|
||||
* Принципы:
|
||||
* - Hexagonal Architecture: это исходящий адаптер (driven adapter)
|
||||
* - Инкапсулирует детали выполнения скриптов через bash
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BashScriptAdapter implements ScriptExecutor {
|
||||
|
||||
private static final String TEMP_DIR = "./temp/";
|
||||
private final HistoryRepository historyRepository;
|
||||
|
||||
public BashScriptAdapter(HistoryRepository historyRepository) {
|
||||
this.historyRepository = historyRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean runInitScript(UUID contextId, String script) {
|
||||
try {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_RUN_START", "Init script execution started");
|
||||
boolean result = runScript(contextId, script);
|
||||
|
||||
if (result) {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_RUN_SUCCESS", "Init script completed successfully");
|
||||
} else {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_RUN_FAILED", "Init script failed with non-zero exit code");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.error("Init script error: {}", ex.getMessage());
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_RUN_ERROR", "Init script error: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean runVerificationScript(UUID contextId, String script) {
|
||||
try {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_CHECK_START", "Verification script execution started");
|
||||
boolean result = runScript(contextId, script);
|
||||
|
||||
if (result) {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_CHECK_SUCCESS", "Verification script completed successfully");
|
||||
} else {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_CHECK_FAILED", "Verification script failed");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.error("Verification script error: {}", ex.getMessage());
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_CHECK_ERROR", "Verification script error: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean runCleanupScript(UUID contextId, String script) {
|
||||
try {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_DELETE_START", "Cleanup script execution started");
|
||||
boolean result = runScript(contextId, script);
|
||||
|
||||
if (result) {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_DELETE_SUCCESS", "Cleanup script completed successfully");
|
||||
} else {
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_DELETE_FAILED", "Cleanup script failed");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.error("Cleanup script error: {}", ex.getMessage());
|
||||
historyRepository.logAction(null, contextId, "SCRIPT_DELETE_ERROR", "Cleanup script error: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean runScript(UUID contextId, String script) throws Exception {
|
||||
var processBuilder = new ProcessBuilder();
|
||||
processBuilder.command("bash", saveFile(contextId, script));
|
||||
|
||||
var process = processBuilder.start();
|
||||
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream())
|
||||
);
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.append(line).append("\n");
|
||||
}
|
||||
log.info("Script output: {}", output);
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
return exitCode == 0;
|
||||
}
|
||||
|
||||
private String saveFile(UUID contextId, String script) throws IOException {
|
||||
Path path = Paths.get(TEMP_DIR + contextId.toString());
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
Files.createDirectories(path);
|
||||
}
|
||||
var tm = System.currentTimeMillis();
|
||||
var scriptPath = Files.write(
|
||||
Paths.get(TEMP_DIR + contextId + "/" + tm),
|
||||
script.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return scriptPath.toString();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue