From 498ba86305b10d600042449ca0d606f0fd907ee6 Mon Sep 17 00:00:00 2001 From: Anton Dzyk Date: Sun, 11 Jan 2026 12:50:55 +0300 Subject: [PATCH] =?UTF-8?q?refactoring,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D0=B2,=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=B8=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/LaunchController.java | 108 ++++++--------- .../controller/TaskController.java | 32 ++--- .../application/domain/carecase/GetTask.java | 5 - .../dto}/auth/IdTokenPayload.java | 2 +- .../dto}/auth/LaunchPresentation.java | 2 +- .../dto}/auth/LtiLogin.java | 2 +- .../dto}/auth/NamesRoleService.java | 2 +- .../dto}/auth/Payload.java | 2 +- .../dto}/auth/ResourceLink.java | 2 +- .../model => application/dto}/auth/Scope.java | 2 +- .../dto}/auth/TokenContext.java | 2 +- .../dto}/auth/TokenEndpoint.java | 2 +- .../dto/result}/GradingType.java | 2 +- .../dto/result}/Lineitems.java | 2 +- .../dto/result}/ProgressType.java | 2 +- .../dto/result}/ResultRequest.java | 2 +- .../dto/result}/ResultResponse.java | 2 +- .../dto/task/CheckTaskRequest.java | 9 ++ .../dto}/task/RequestUpdateTask.java | 2 +- .../dto}/task/TaskData.java | 2 +- .../dto}/task/TaskQueueStatus.java | 2 +- .../dto}/task/TaskResult.java | 2 +- .../dto}/task/TaskType.java | 2 +- .../infrastructure/lms/LMSService.java | 9 -- .../infrastructure/runner/Runner.java | 12 -- .../infrastructure/runner/RunnerImpl.java | 110 --------------- .../service/history/HistoryService.java | 25 ---- .../service/history/HistoryServiceImpl.java | 61 -------- .../application/service/jwt/JwtService.java | 4 +- .../service/jwt/JwtServiceImpl.java | 2 +- .../service/results/ResultServiceImpl.java | 6 +- .../application/service/task/TaskService.java | 10 +- .../service/task/TaskServiceImpl.java | 68 ++++----- .../usecase/lti/HandleLtiRedirectUseCase.java | 48 +++++++ .../usecase/lti/ProcessLtiLoginUseCase.java | 88 ++++++++++++ .../oa2/lti/domain/model/ResultRequest.java | 8 -- .../domain/port/out/HistoryRepository.java | 31 +++++ .../oa2/lti/domain/port/out/LMSGateway.java | 28 ++++ .../lti/domain/port/out/ScriptExecutor.java | 38 +++++ .../out}/TaskRepository.java | 2 +- .../adapter/history/HistoryJpaAdapter.java | 81 +++++++++++ .../adapter/lms/LMSRestAdapter.java} | 35 ++--- .../jpa/TaskRepositoryAdapter.java | 2 +- .../adapter/script/BashScriptAdapter.java | 130 ++++++++++++++++++ 44 files changed, 585 insertions(+), 405 deletions(-) delete mode 100644 src/main/java/ru/oa2/lti/application/domain/carecase/GetTask.java rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/IdTokenPayload.java (92%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/LaunchPresentation.java (82%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/LtiLogin.java (84%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/NamesRoleService.java (90%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/Payload.java (89%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/ResourceLink.java (70%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/Scope.java (93%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/TokenContext.java (80%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/auth/TokenEndpoint.java (94%) rename src/main/java/ru/oa2/lti/{domain/model/results => application/dto/result}/GradingType.java (70%) rename src/main/java/ru/oa2/lti/{domain/model/results => application/dto/result}/Lineitems.java (69%) rename src/main/java/ru/oa2/lti/{domain/model/results => application/dto/result}/ProgressType.java (67%) rename src/main/java/ru/oa2/lti/{domain/model/results => application/dto/result}/ResultRequest.java (87%) rename src/main/java/ru/oa2/lti/{domain/model/results => application/dto/result}/ResultResponse.java (57%) create mode 100644 src/main/java/ru/oa2/lti/application/dto/task/CheckTaskRequest.java rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/task/RequestUpdateTask.java (79%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/task/TaskData.java (87%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/task/TaskQueueStatus.java (65%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/task/TaskResult.java (75%) rename src/main/java/ru/oa2/lti/{domain/model => application/dto}/task/TaskType.java (56%) delete mode 100644 src/main/java/ru/oa2/lti/application/infrastructure/lms/LMSService.java delete mode 100644 src/main/java/ru/oa2/lti/application/infrastructure/runner/Runner.java delete mode 100644 src/main/java/ru/oa2/lti/application/infrastructure/runner/RunnerImpl.java delete mode 100644 src/main/java/ru/oa2/lti/application/service/history/HistoryService.java delete mode 100644 src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java create mode 100644 src/main/java/ru/oa2/lti/application/usecase/lti/HandleLtiRedirectUseCase.java create mode 100644 src/main/java/ru/oa2/lti/application/usecase/lti/ProcessLtiLoginUseCase.java delete mode 100644 src/main/java/ru/oa2/lti/domain/model/ResultRequest.java create mode 100644 src/main/java/ru/oa2/lti/domain/port/out/HistoryRepository.java create mode 100644 src/main/java/ru/oa2/lti/domain/port/out/LMSGateway.java create mode 100644 src/main/java/ru/oa2/lti/domain/port/out/ScriptExecutor.java rename src/main/java/ru/oa2/lti/domain/{repository => port/out}/TaskRepository.java (98%) create mode 100644 src/main/java/ru/oa2/lti/infrastructure/adapter/history/HistoryJpaAdapter.java rename src/main/java/ru/oa2/lti/{application/infrastructure/lms/LMSServiceImpl.java => infrastructure/adapter/lms/LMSRestAdapter.java} (75%) create mode 100644 src/main/java/ru/oa2/lti/infrastructure/adapter/script/BashScriptAdapter.java diff --git a/src/main/java/ru/oa2/lti/application/controller/LaunchController.java b/src/main/java/ru/oa2/lti/application/controller/LaunchController.java index 5e296c7..86a3fcb 100644 --- a/src/main/java/ru/oa2/lti/application/controller/LaunchController.java +++ b/src/main/java/ru/oa2/lti/application/controller/LaunchController.java @@ -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(); } } diff --git a/src/main/java/ru/oa2/lti/application/controller/TaskController.java b/src/main/java/ru/oa2/lti/application/controller/TaskController.java index b5ca50a..d1ad564 100644 --- a/src/main/java/ru/oa2/lti/application/controller/TaskController.java +++ b/src/main/java/ru/oa2/lti/application/controller/TaskController.java @@ -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))); } } diff --git a/src/main/java/ru/oa2/lti/application/domain/carecase/GetTask.java b/src/main/java/ru/oa2/lti/application/domain/carecase/GetTask.java deleted file mode 100644 index fcf909e..0000000 --- a/src/main/java/ru/oa2/lti/application/domain/carecase/GetTask.java +++ /dev/null @@ -1,5 +0,0 @@ -package ru.oa2.lti.application.domain.carecase; - -public class GetTask { - -} diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/IdTokenPayload.java b/src/main/java/ru/oa2/lti/application/dto/auth/IdTokenPayload.java similarity index 92% rename from src/main/java/ru/oa2/lti/domain/model/auth/IdTokenPayload.java rename to src/main/java/ru/oa2/lti/application/dto/auth/IdTokenPayload.java index d84fdb0..7b9fd03 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/IdTokenPayload.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/IdTokenPayload.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.auth; +package ru.oa2.lti.application.dto.auth; import lombok.Data; diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/LaunchPresentation.java b/src/main/java/ru/oa2/lti/application/dto/auth/LaunchPresentation.java similarity index 82% rename from src/main/java/ru/oa2/lti/domain/model/auth/LaunchPresentation.java rename to src/main/java/ru/oa2/lti/application/dto/auth/LaunchPresentation.java index b603a91..3c94167 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/LaunchPresentation.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/LaunchPresentation.java @@ -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" : { diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/LtiLogin.java b/src/main/java/ru/oa2/lti/application/dto/auth/LtiLogin.java similarity index 84% rename from src/main/java/ru/oa2/lti/domain/model/auth/LtiLogin.java rename to src/main/java/ru/oa2/lti/application/dto/auth/LtiLogin.java index 236f834..1d50c27 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/LtiLogin.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/LtiLogin.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.auth; +package ru.oa2.lti.application.dto.auth; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/NamesRoleService.java b/src/main/java/ru/oa2/lti/application/dto/auth/NamesRoleService.java similarity index 90% rename from src/main/java/ru/oa2/lti/domain/model/auth/NamesRoleService.java rename to src/main/java/ru/oa2/lti/application/dto/auth/NamesRoleService.java index 80995a0..47c1d91 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/NamesRoleService.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/NamesRoleService.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.auth; +package ru.oa2.lti.application.dto.auth; import java.util.List; diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/Payload.java b/src/main/java/ru/oa2/lti/application/dto/auth/Payload.java similarity index 89% rename from src/main/java/ru/oa2/lti/domain/model/auth/Payload.java rename to src/main/java/ru/oa2/lti/application/dto/auth/Payload.java index b3dc1ca..1a35619 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/Payload.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/Payload.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.auth; +package ru.oa2.lti.application.dto.auth; import lombok.Data; import lombok.NoArgsConstructor; diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/ResourceLink.java b/src/main/java/ru/oa2/lti/application/dto/auth/ResourceLink.java similarity index 70% rename from src/main/java/ru/oa2/lti/domain/model/auth/ResourceLink.java rename to src/main/java/ru/oa2/lti/application/dto/auth/ResourceLink.java index fb4e261..c9e9560 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/ResourceLink.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/ResourceLink.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.auth; +package ru.oa2.lti.application.dto.auth; import java.util.UUID; diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/Scope.java b/src/main/java/ru/oa2/lti/application/dto/auth/Scope.java similarity index 93% rename from src/main/java/ru/oa2/lti/domain/model/auth/Scope.java rename to src/main/java/ru/oa2/lti/application/dto/auth/Scope.java index edb800b..24b8d61 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/Scope.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/Scope.java @@ -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; diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/TokenContext.java b/src/main/java/ru/oa2/lti/application/dto/auth/TokenContext.java similarity index 80% rename from src/main/java/ru/oa2/lti/domain/model/auth/TokenContext.java rename to src/main/java/ru/oa2/lti/application/dto/auth/TokenContext.java index 8a89ccc..5fa9137 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/TokenContext.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/TokenContext.java @@ -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; diff --git a/src/main/java/ru/oa2/lti/domain/model/auth/TokenEndpoint.java b/src/main/java/ru/oa2/lti/application/dto/auth/TokenEndpoint.java similarity index 94% rename from src/main/java/ru/oa2/lti/domain/model/auth/TokenEndpoint.java rename to src/main/java/ru/oa2/lti/application/dto/auth/TokenEndpoint.java index f6013e8..e7a2fbe 100644 --- a/src/main/java/ru/oa2/lti/domain/model/auth/TokenEndpoint.java +++ b/src/main/java/ru/oa2/lti/application/dto/auth/TokenEndpoint.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.auth; +package ru.oa2.lti.application.dto.auth; import java.util.List; diff --git a/src/main/java/ru/oa2/lti/domain/model/results/GradingType.java b/src/main/java/ru/oa2/lti/application/dto/result/GradingType.java similarity index 70% rename from src/main/java/ru/oa2/lti/domain/model/results/GradingType.java rename to src/main/java/ru/oa2/lti/application/dto/result/GradingType.java index b377893..aca2c1f 100644 --- a/src/main/java/ru/oa2/lti/domain/model/results/GradingType.java +++ b/src/main/java/ru/oa2/lti/application/dto/result/GradingType.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.results; +package ru.oa2.lti.application.dto.result; public enum GradingType { NotStarted, diff --git a/src/main/java/ru/oa2/lti/domain/model/results/Lineitems.java b/src/main/java/ru/oa2/lti/application/dto/result/Lineitems.java similarity index 69% rename from src/main/java/ru/oa2/lti/domain/model/results/Lineitems.java rename to src/main/java/ru/oa2/lti/application/dto/result/Lineitems.java index ce31bc5..96e2a11 100644 --- a/src/main/java/ru/oa2/lti/domain/model/results/Lineitems.java +++ b/src/main/java/ru/oa2/lti/application/dto/result/Lineitems.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.results; +package ru.oa2.lti.application.dto.result; public record Lineitems( String id, diff --git a/src/main/java/ru/oa2/lti/domain/model/results/ProgressType.java b/src/main/java/ru/oa2/lti/application/dto/result/ProgressType.java similarity index 67% rename from src/main/java/ru/oa2/lti/domain/model/results/ProgressType.java rename to src/main/java/ru/oa2/lti/application/dto/result/ProgressType.java index ac12d05..5fb0f2c 100644 --- a/src/main/java/ru/oa2/lti/domain/model/results/ProgressType.java +++ b/src/main/java/ru/oa2/lti/application/dto/result/ProgressType.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.results; +package ru.oa2.lti.application.dto.result; public enum ProgressType { Initialized, diff --git a/src/main/java/ru/oa2/lti/domain/model/results/ResultRequest.java b/src/main/java/ru/oa2/lti/application/dto/result/ResultRequest.java similarity index 87% rename from src/main/java/ru/oa2/lti/domain/model/results/ResultRequest.java rename to src/main/java/ru/oa2/lti/application/dto/result/ResultRequest.java index aae8306..c4b93d6 100644 --- a/src/main/java/ru/oa2/lti/domain/model/results/ResultRequest.java +++ b/src/main/java/ru/oa2/lti/application/dto/result/ResultRequest.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.results; +package ru.oa2.lti.application.dto.result; import lombok.Builder; diff --git a/src/main/java/ru/oa2/lti/domain/model/results/ResultResponse.java b/src/main/java/ru/oa2/lti/application/dto/result/ResultResponse.java similarity index 57% rename from src/main/java/ru/oa2/lti/domain/model/results/ResultResponse.java rename to src/main/java/ru/oa2/lti/application/dto/result/ResultResponse.java index 2eb4a05..20f962e 100644 --- a/src/main/java/ru/oa2/lti/domain/model/results/ResultResponse.java +++ b/src/main/java/ru/oa2/lti/application/dto/result/ResultResponse.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.results; +package ru.oa2.lti.application.dto.result; public record ResultResponse( String status diff --git a/src/main/java/ru/oa2/lti/application/dto/task/CheckTaskRequest.java b/src/main/java/ru/oa2/lti/application/dto/task/CheckTaskRequest.java new file mode 100644 index 0000000..f4f003d --- /dev/null +++ b/src/main/java/ru/oa2/lti/application/dto/task/CheckTaskRequest.java @@ -0,0 +1,9 @@ +package ru.oa2.lti.application.dto.task; + +/** + * DTO для запроса проверки задачи + */ +public record CheckTaskRequest( + String clientId, + String idToken +) {} diff --git a/src/main/java/ru/oa2/lti/domain/model/task/RequestUpdateTask.java b/src/main/java/ru/oa2/lti/application/dto/task/RequestUpdateTask.java similarity index 79% rename from src/main/java/ru/oa2/lti/domain/model/task/RequestUpdateTask.java rename to src/main/java/ru/oa2/lti/application/dto/task/RequestUpdateTask.java index c166175..f017ece 100644 --- a/src/main/java/ru/oa2/lti/domain/model/task/RequestUpdateTask.java +++ b/src/main/java/ru/oa2/lti/application/dto/task/RequestUpdateTask.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.task; +package ru.oa2.lti.application.dto.task; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/ru/oa2/lti/domain/model/task/TaskData.java b/src/main/java/ru/oa2/lti/application/dto/task/TaskData.java similarity index 87% rename from src/main/java/ru/oa2/lti/domain/model/task/TaskData.java rename to src/main/java/ru/oa2/lti/application/dto/task/TaskData.java index daeaf07..cc24233 100644 --- a/src/main/java/ru/oa2/lti/domain/model/task/TaskData.java +++ b/src/main/java/ru/oa2/lti/application/dto/task/TaskData.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.task; +package ru.oa2.lti.application.dto.task; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/ru/oa2/lti/domain/model/task/TaskQueueStatus.java b/src/main/java/ru/oa2/lti/application/dto/task/TaskQueueStatus.java similarity index 65% rename from src/main/java/ru/oa2/lti/domain/model/task/TaskQueueStatus.java rename to src/main/java/ru/oa2/lti/application/dto/task/TaskQueueStatus.java index e3690a3..350f1bf 100644 --- a/src/main/java/ru/oa2/lti/domain/model/task/TaskQueueStatus.java +++ b/src/main/java/ru/oa2/lti/application/dto/task/TaskQueueStatus.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.task; +package ru.oa2.lti.application.dto.task; public enum TaskQueueStatus { NEW, diff --git a/src/main/java/ru/oa2/lti/domain/model/task/TaskResult.java b/src/main/java/ru/oa2/lti/application/dto/task/TaskResult.java similarity index 75% rename from src/main/java/ru/oa2/lti/domain/model/task/TaskResult.java rename to src/main/java/ru/oa2/lti/application/dto/task/TaskResult.java index 3ba3d9a..33394b9 100644 --- a/src/main/java/ru/oa2/lti/domain/model/task/TaskResult.java +++ b/src/main/java/ru/oa2/lti/application/dto/task/TaskResult.java @@ -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:, оценка diff --git a/src/main/java/ru/oa2/lti/domain/model/task/TaskType.java b/src/main/java/ru/oa2/lti/application/dto/task/TaskType.java similarity index 56% rename from src/main/java/ru/oa2/lti/domain/model/task/TaskType.java rename to src/main/java/ru/oa2/lti/application/dto/task/TaskType.java index cbfed09..9a7f582 100644 --- a/src/main/java/ru/oa2/lti/domain/model/task/TaskType.java +++ b/src/main/java/ru/oa2/lti/application/dto/task/TaskType.java @@ -1,4 +1,4 @@ -package ru.oa2.lti.domain.model.task; +package ru.oa2.lti.application.dto.task; public enum TaskType { DOCKER, diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/lms/LMSService.java b/src/main/java/ru/oa2/lti/application/infrastructure/lms/LMSService.java deleted file mode 100644 index cbfb68b..0000000 --- a/src/main/java/ru/oa2/lti/application/infrastructure/lms/LMSService.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/runner/Runner.java b/src/main/java/ru/oa2/lti/application/infrastructure/runner/Runner.java deleted file mode 100644 index 9972257..0000000 --- a/src/main/java/ru/oa2/lti/application/infrastructure/runner/Runner.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/runner/RunnerImpl.java b/src/main/java/ru/oa2/lti/application/infrastructure/runner/RunnerImpl.java deleted file mode 100644 index 3848287..0000000 --- a/src/main/java/ru/oa2/lti/application/infrastructure/runner/RunnerImpl.java +++ /dev/null @@ -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(); - } -} diff --git a/src/main/java/ru/oa2/lti/application/service/history/HistoryService.java b/src/main/java/ru/oa2/lti/application/service/history/HistoryService.java deleted file mode 100644 index 5e3868c..0000000 --- a/src/main/java/ru/oa2/lti/application/service/history/HistoryService.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java b/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java deleted file mode 100644 index f0ea55d..0000000 --- a/src/main/java/ru/oa2/lti/application/service/history/HistoryServiceImpl.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/ru/oa2/lti/application/service/jwt/JwtService.java b/src/main/java/ru/oa2/lti/application/service/jwt/JwtService.java index 9d23a49..f4f0231 100644 --- a/src/main/java/ru/oa2/lti/application/service/jwt/JwtService.java +++ b/src/main/java/ru/oa2/lti/application/service/jwt/JwtService.java @@ -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 { diff --git a/src/main/java/ru/oa2/lti/application/service/jwt/JwtServiceImpl.java b/src/main/java/ru/oa2/lti/application/service/jwt/JwtServiceImpl.java index 8584165..5ec0cb6 100644 --- a/src/main/java/ru/oa2/lti/application/service/jwt/JwtServiceImpl.java +++ b/src/main/java/ru/oa2/lti/application/service/jwt/JwtServiceImpl.java @@ -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; diff --git a/src/main/java/ru/oa2/lti/application/service/results/ResultServiceImpl.java b/src/main/java/ru/oa2/lti/application/service/results/ResultServiceImpl.java index 6acb1f3..80cf6f7 100644 --- a/src/main/java/ru/oa2/lti/application/service/results/ResultServiceImpl.java +++ b/src/main/java/ru/oa2/lti/application/service/results/ResultServiceImpl.java @@ -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; diff --git a/src/main/java/ru/oa2/lti/application/service/task/TaskService.java b/src/main/java/ru/oa2/lti/application/service/task/TaskService.java index 3581801..04457b9 100644 --- a/src/main/java/ru/oa2/lti/application/service/task/TaskService.java +++ b/src/main/java/ru/oa2/lti/application/service/task/TaskService.java @@ -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-а diff --git a/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java b/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java index 2170f85..e07fc4c 100644 --- a/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java +++ b/src/main/java/ru/oa2/lti/application/service/task/TaskServiceImpl.java @@ -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())); } diff --git a/src/main/java/ru/oa2/lti/application/usecase/lti/HandleLtiRedirectUseCase.java b/src/main/java/ru/oa2/lti/application/usecase/lti/HandleLtiRedirectUseCase.java new file mode 100644 index 0000000..f5e4bd8 --- /dev/null +++ b/src/main/java/ru/oa2/lti/application/usecase/lti/HandleLtiRedirectUseCase.java @@ -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"); + } +} diff --git a/src/main/java/ru/oa2/lti/application/usecase/lti/ProcessLtiLoginUseCase.java b/src/main/java/ru/oa2/lti/application/usecase/lti/ProcessLtiLoginUseCase.java new file mode 100644 index 0000000..7bda044 --- /dev/null +++ b/src/main/java/ru/oa2/lti/application/usecase/lti/ProcessLtiLoginUseCase.java @@ -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); + } +} diff --git a/src/main/java/ru/oa2/lti/domain/model/ResultRequest.java b/src/main/java/ru/oa2/lti/domain/model/ResultRequest.java deleted file mode 100644 index 78189cb..0000000 --- a/src/main/java/ru/oa2/lti/domain/model/ResultRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package ru.oa2.lti.domain.model; - -//TODO 2 одинаковых имени? ) -public record ResultRequest( - String clientId, - String idToken -) { -} diff --git a/src/main/java/ru/oa2/lti/domain/port/out/HistoryRepository.java b/src/main/java/ru/oa2/lti/domain/port/out/HistoryRepository.java new file mode 100644 index 0000000..9cc5315 --- /dev/null +++ b/src/main/java/ru/oa2/lti/domain/port/out/HistoryRepository.java @@ -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); +} diff --git a/src/main/java/ru/oa2/lti/domain/port/out/LMSGateway.java b/src/main/java/ru/oa2/lti/domain/port/out/LMSGateway.java new file mode 100644 index 0000000..f88e1e1 --- /dev/null +++ b/src/main/java/ru/oa2/lti/domain/port/out/LMSGateway.java @@ -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); +} diff --git a/src/main/java/ru/oa2/lti/domain/port/out/ScriptExecutor.java b/src/main/java/ru/oa2/lti/domain/port/out/ScriptExecutor.java new file mode 100644 index 0000000..23af6a7 --- /dev/null +++ b/src/main/java/ru/oa2/lti/domain/port/out/ScriptExecutor.java @@ -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); +} diff --git a/src/main/java/ru/oa2/lti/domain/repository/TaskRepository.java b/src/main/java/ru/oa2/lti/domain/port/out/TaskRepository.java similarity index 98% rename from src/main/java/ru/oa2/lti/domain/repository/TaskRepository.java rename to src/main/java/ru/oa2/lti/domain/port/out/TaskRepository.java index cbcda2b..0a25570 100644 --- a/src/main/java/ru/oa2/lti/domain/repository/TaskRepository.java +++ b/src/main/java/ru/oa2/lti/domain/port/out/TaskRepository.java @@ -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; diff --git a/src/main/java/ru/oa2/lti/infrastructure/adapter/history/HistoryJpaAdapter.java b/src/main/java/ru/oa2/lti/infrastructure/adapter/history/HistoryJpaAdapter.java new file mode 100644 index 0000000..1621c3a --- /dev/null +++ b/src/main/java/ru/oa2/lti/infrastructure/adapter/history/HistoryJpaAdapter.java @@ -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; + } +} diff --git a/src/main/java/ru/oa2/lti/application/infrastructure/lms/LMSServiceImpl.java b/src/main/java/ru/oa2/lti/infrastructure/adapter/lms/LMSRestAdapter.java similarity index 75% rename from src/main/java/ru/oa2/lti/application/infrastructure/lms/LMSServiceImpl.java rename to src/main/java/ru/oa2/lti/infrastructure/adapter/lms/LMSRestAdapter.java index c7e2452..ac90ec5 100644 --- a/src/main/java/ru/oa2/lti/application/infrastructure/lms/LMSServiceImpl.java +++ b/src/main/java/ru/oa2/lti/infrastructure/adapter/lms/LMSRestAdapter.java @@ -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 getBody(String clientAssertion) { - MultiValueMap 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; } } diff --git a/src/main/java/ru/oa2/lti/infrastructure/adapter/persistence/jpa/TaskRepositoryAdapter.java b/src/main/java/ru/oa2/lti/infrastructure/adapter/persistence/jpa/TaskRepositoryAdapter.java index 074d4e8..f9169eb 100644 --- a/src/main/java/ru/oa2/lti/infrastructure/adapter/persistence/jpa/TaskRepositoryAdapter.java +++ b/src/main/java/ru/oa2/lti/infrastructure/adapter/persistence/jpa/TaskRepositoryAdapter.java @@ -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; diff --git a/src/main/java/ru/oa2/lti/infrastructure/adapter/script/BashScriptAdapter.java b/src/main/java/ru/oa2/lti/infrastructure/adapter/script/BashScriptAdapter.java new file mode 100644 index 0000000..bc3246b --- /dev/null +++ b/src/main/java/ru/oa2/lti/infrastructure/adapter/script/BashScriptAdapter.java @@ -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(); + } +}