From 201d604ca2d337e75360b09d3e698d9863537ab7 Mon Sep 17 00:00:00 2001 From: Anton Dzyk Date: Sat, 12 Oct 2024 20:21:38 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=B0=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + LICENSE | 202 + Readme.md | 52 + debug/.env | 3 + debug/Readme.md | 14 + debug/configs/application.properties | 3 + debug/configs/init-db.sh | 14 + debug/docker-compose.yml | 102 + src/backend/api-server/.gitignore | 33 + .../.mvn/wrapper/maven-wrapper.properties | 19 + src/backend/api-server/mvnw | 259 ++ src/backend/api-server/mvnw.cmd | 149 + src/backend/api-server/pom.xml | 181 + .../api/application/ApiServerApplication.java | 24 + .../application/config/ApplicationConfig.java | 15 + .../config/CustomSuccessHandler.java | 34 + .../api/application/config/KafkaConfig.java | 70 + .../edu/api/application/config/SaveUser.java | 35 + .../application/config/SecurityConfig.java | 34 + .../application/controller/ApiController.java | 45 + .../application/controller/RunController.java | 40 + .../controller/ThemeController.java | 37 + .../controller/UserController.java | 37 + .../database/JobJpaRepository.java | 10 + .../database/TaskJpaRepository.java | 10 + .../database/ThemeJpaRepository.java | 10 + .../database/UserJpaRepository.java | 10 + .../database/UserProgressJpaRepository.java | 8 + .../services/RepositoryService.java | 141 + .../application/services/RunnerService.java | 10 + .../services/RunnerServiceImpl.java | 132 + .../api/application/utils/MappingTypes.java | 18 + .../api/application/utils/TaskComparator.java | 12 + .../application/utils/ThemeComparator.java | 12 + .../ru/oa2/edu/api/domain/dto/DataCase.java | 34 + .../oa2/edu/api/domain/dto/EngineTypes.java | 25 + .../java/ru/oa2/edu/api/domain/dto/Job.java | 32 + .../ru/oa2/edu/api/domain/dto/JobResult.java | 29 + .../ru/oa2/edu/api/domain/dto/JobStatus.java | 6 + .../oa2/edu/api/domain/dto/RequestData.java | 19 + .../java/ru/oa2/edu/api/domain/dto/Run.java | 19 + .../ru/oa2/edu/api/domain/dto/TestCase.java | 21 + .../oa2/edu/api/domain/dto/UserProgress.java | 30 + .../java/ru/oa2/edu/api/domain/job/Job.java | 110 + .../oa2/edu/api/domain/job/JobRepository.java | 8 + .../edu/api/domain/job/JobRepositoryImpl.java | 21 + .../java/ru/oa2/edu/api/domain/task/Task.java | 62 + .../ru/oa2/edu/api/domain/task/TaskDTO.java | 20 + .../edu/api/domain/task/TaskRepository.java | 10 + .../ru/oa2/edu/api/domain/theme/Theme.java | 77 + .../ru/oa2/edu/api/domain/theme/ThemeDTO.java | 25 + .../edu/api/domain/theme/ThemeRepository.java | 9 + .../ru/oa2/edu/api/domain/user/StatJob.java | 13 + .../java/ru/oa2/edu/api/domain/user/User.java | 30 + .../ru/oa2/edu/api/domain/user/UserDTO.java | 13 + .../api/domain/user/UserDataStatistic.java | 40 + .../ru/oa2/edu/api/domain/user/UserInfo.java | 7 + .../oa2/edu/api/domain/user/UserProgress.java | 74 + .../domain/user/UserProgressRepository.java | 4 + .../edu/api/domain/user/UserRepository.java | 10 + .../edu/api/domain/user/UserStatistic.java | 6 + .../src/main/resources/application.yml | 45 + .../src/main/resources/liquibase/Dockerfile | 5 + .../src/main/resources/liquibase/Readme.md | 33 + .../db/1.0.0/db.20240831-1-create_theme.xml | 40 + .../db/1.0.0/db.20240831-2-create_list.xml | 26 + .../db/1.0.0/db.20240831-3-create_task.xml | 44 + .../db.20240831-4-create_internal_user.xml | 28 + .../db.20240831-5-create_user_progress.xml | 44 + .../db/1.0.0/db.20240919-6-create_job.xml | 52 + .../db/1.0.0/db.20240919-7-create_journal.xml | 40 + .../liquibase/db/1.0.0/db.version-master.xml | 17 + .../resources/liquibase/db/db.root-master.xml | 12 + .../demo/1.0.0/demo.20240921-1-add_themes.xml | 39 + .../demo/1.0.0/demo.20240921-2-add_tasks.xml | 151 + .../demo/1.0.0/demo.20240921-3-add_lists.xml | 81 + .../demo/1.0.0/demo.version-master.xml | 14 + .../liquibase/demo/demo.root-master.xml | 12 + .../main/resources/mappings/internal_user.xml | 24 + .../src/main/resources/mappings/job.xml | 49 + .../src/main/resources/mappings/task.xml | 40 + .../src/main/resources/mappings/theme.xml | 50 + .../main/resources/mappings/user_progress.xml | 39 + .../test/java/ru/oa2/edu/api/ThemeTest.java | 23 + .../case-services/java-http-ok/pom.xml | 57 + .../main/java/ru/edu/portal/OkHttpServer.java | 56 + src/backend/engines/docker/.gitignore | 2 + src/backend/engines/docker/api.go | 32 + src/backend/engines/docker/build.go | 125 + src/backend/engines/docker/build_test.go | 59 + src/backend/engines/docker/client.go | 23 + src/backend/engines/docker/containers.go | 36 + src/backend/engines/docker/containers_test.go | 67 + .../docker/contexts/base/case1/app.jar | Bin 0 -> 5020 bytes .../docker/contexts/base/case1/request.json | 19 + .../docker/contexts/base/case2/app.jar | Bin 0 -> 5020 bytes .../docker/contexts/base/case2/request.json | 19 + .../docker/contexts/base/case3/main.py | 10 + .../docker/contexts/base/case3/request.json | 22 + .../contexts/base/case3/requirements.txt | 1 + .../docker/contexts/base/case4/Dockerfile | 7 + .../docker/contexts/base/case4/app.jar | Bin 0 -> 5020 bytes .../docker/contexts/base/case4/request.json | 22 + .../docker/contexts/base/case5/Dockerfile | 10 + .../docker/contexts/base/case5/case5.go | 18 + .../docker/contexts/base/case5/request.json | 22 + .../docker/contexts/base/case_1_back.json | 26 + src/backend/engines/docker/go.mod | 72 + src/backend/engines/docker/go.sum | 206 + src/backend/engines/docker/image.go | 11 + src/backend/engines/docker/kafka_client.go | 69 + src/backend/engines/docker/main.go | 21 + src/backend/engines/docker/network.go | 22 + src/backend/engines/docker/prune.go | 37 + src/backend/engines/docker/show_env.go | 36 + src/backend/engines/docker/struct.go | 75 + src/backend/engines/docker/struct_test.go | 22 + src/backend/engines/docker/task_runner.go | 139 + src/backend/engines/docker/test_engine.go | 145 + src/frontend/user-frontend/.eslintrc.cjs | 14 + src/frontend/user-frontend/Dockerfile | 14 + src/frontend/user-frontend/README.md | 39 + src/frontend/user-frontend/components.d.ts | 25 + src/frontend/user-frontend/env.d.ts | 1 + src/frontend/user-frontend/index.html | 13 + src/frontend/user-frontend/jsconfig.json | 20 + src/frontend/user-frontend/nginx.conf | 27 + src/frontend/user-frontend/package-lock.json | 3651 +++++++++++++++++ src/frontend/user-frontend/package.json | 41 + src/frontend/user-frontend/src/App.vue | 67 + .../user-frontend/src/assets/base.css | 86 + .../user-frontend/src/assets/logo.ico | Bin 0 -> 1150 bytes .../user-frontend/src/assets/logo.svg | 11 + .../user-frontend/src/assets/main.css | 30 + .../src/assets/project_goals.png | Bin 0 -> 42254 bytes .../user-frontend/src/components/HomePage.vue | 60 + .../user-frontend/src/components/Profile.vue | 20 + .../user-frontend/src/components/Runner.vue | 156 + .../user-frontend/src/components/Tasks.vue | 32 + .../user-frontend/src/components/Themes.vue | 61 + .../src/components/WelcomeItem.vue | 87 + .../src/components/icons/IconCommunity.vue | 7 + .../components/icons/IconDocumentation.vue | 7 + .../src/components/icons/IconEcosystem.vue | 7 + .../src/components/icons/IconSupport.vue | 7 + .../src/components/icons/IconTooling.vue | 19 + src/frontend/user-frontend/src/main.ts | 23 + .../user-frontend/src/plugins/axios.js | 22 + .../user-frontend/src/plugins/index.js | 9 + .../user-frontend/src/plugins/keycloak.js | 40 + .../user-frontend/src/plugins/router.js | 9 + .../user-frontend/src/router/index.ts | 45 + .../user-frontend/src/views/AboutView.vue | 42 + .../user-frontend/src/views/ErrorView.vue | 15 + .../user-frontend/src/views/HomeView.vue | 10 + src/frontend/user-frontend/tsconfig.app.json | 14 + src/frontend/user-frontend/tsconfig.json | 11 + src/frontend/user-frontend/tsconfig.node.json | 19 + src/frontend/user-frontend/typed-router.d.ts | 22 + src/frontend/user-frontend/vite.config.mjs | 54 + src/frontend/user-frontend/vite.config.ts | 16 + 161 files changed, 9783 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Readme.md create mode 100644 debug/.env create mode 100644 debug/Readme.md create mode 100644 debug/configs/application.properties create mode 100755 debug/configs/init-db.sh create mode 100644 debug/docker-compose.yml create mode 100644 src/backend/api-server/.gitignore create mode 100644 src/backend/api-server/.mvn/wrapper/maven-wrapper.properties create mode 100755 src/backend/api-server/mvnw create mode 100644 src/backend/api-server/mvnw.cmd create mode 100644 src/backend/api-server/pom.xml create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/ApiServerApplication.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/ApplicationConfig.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/CustomSuccessHandler.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/KafkaConfig.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/SaveUser.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/SecurityConfig.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/ApiController.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/RunController.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/ThemeController.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/UserController.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/JobJpaRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/TaskJpaRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/ThemeJpaRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/UserJpaRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/UserProgressJpaRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RepositoryService.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RunnerService.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RunnerServiceImpl.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/MappingTypes.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/TaskComparator.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/ThemeComparator.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/DataCase.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/EngineTypes.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/Job.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/JobResult.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/JobStatus.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/RequestData.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/Run.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/TestCase.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/UserProgress.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/Job.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/JobRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/JobRepositoryImpl.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/Task.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/TaskDTO.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/TaskRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/Theme.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/ThemeDTO.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/ThemeRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/StatJob.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/User.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserDTO.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserDataStatistic.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserInfo.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserProgress.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserProgressRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserRepository.java create mode 100644 src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserStatistic.java create mode 100644 src/backend/api-server/src/main/resources/application.yml create mode 100644 src/backend/api-server/src/main/resources/liquibase/Dockerfile create mode 100644 src/backend/api-server/src/main/resources/liquibase/Readme.md create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-1-create_theme.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-2-create_list.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-3-create_task.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-4-create_internal_user.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-5-create_user_progress.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240919-6-create_job.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240919-7-create_journal.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.version-master.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/db/db.root-master.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-1-add_themes.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-2-add_tasks.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-3-add_lists.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.version-master.xml create mode 100644 src/backend/api-server/src/main/resources/liquibase/demo/demo.root-master.xml create mode 100644 src/backend/api-server/src/main/resources/mappings/internal_user.xml create mode 100644 src/backend/api-server/src/main/resources/mappings/job.xml create mode 100644 src/backend/api-server/src/main/resources/mappings/task.xml create mode 100644 src/backend/api-server/src/main/resources/mappings/theme.xml create mode 100644 src/backend/api-server/src/main/resources/mappings/user_progress.xml create mode 100644 src/backend/api-server/src/test/java/ru/oa2/edu/api/ThemeTest.java create mode 100644 src/backend/case-services/java-http-ok/pom.xml create mode 100644 src/backend/case-services/java-http-ok/src/main/java/ru/edu/portal/OkHttpServer.java create mode 100755 src/backend/engines/docker/.gitignore create mode 100644 src/backend/engines/docker/api.go create mode 100755 src/backend/engines/docker/build.go create mode 100755 src/backend/engines/docker/build_test.go create mode 100755 src/backend/engines/docker/client.go create mode 100755 src/backend/engines/docker/containers.go create mode 100755 src/backend/engines/docker/containers_test.go create mode 100644 src/backend/engines/docker/contexts/base/case1/app.jar create mode 100644 src/backend/engines/docker/contexts/base/case1/request.json create mode 100644 src/backend/engines/docker/contexts/base/case2/app.jar create mode 100644 src/backend/engines/docker/contexts/base/case2/request.json create mode 100755 src/backend/engines/docker/contexts/base/case3/main.py create mode 100644 src/backend/engines/docker/contexts/base/case3/request.json create mode 100755 src/backend/engines/docker/contexts/base/case3/requirements.txt create mode 100644 src/backend/engines/docker/contexts/base/case4/Dockerfile create mode 100644 src/backend/engines/docker/contexts/base/case4/app.jar create mode 100644 src/backend/engines/docker/contexts/base/case4/request.json create mode 100644 src/backend/engines/docker/contexts/base/case5/Dockerfile create mode 100644 src/backend/engines/docker/contexts/base/case5/case5.go create mode 100644 src/backend/engines/docker/contexts/base/case5/request.json create mode 100644 src/backend/engines/docker/contexts/base/case_1_back.json create mode 100755 src/backend/engines/docker/go.mod create mode 100755 src/backend/engines/docker/go.sum create mode 100644 src/backend/engines/docker/image.go create mode 100644 src/backend/engines/docker/kafka_client.go create mode 100755 src/backend/engines/docker/main.go create mode 100755 src/backend/engines/docker/network.go create mode 100755 src/backend/engines/docker/prune.go create mode 100755 src/backend/engines/docker/show_env.go create mode 100644 src/backend/engines/docker/struct.go create mode 100644 src/backend/engines/docker/struct_test.go create mode 100644 src/backend/engines/docker/task_runner.go create mode 100755 src/backend/engines/docker/test_engine.go create mode 100644 src/frontend/user-frontend/.eslintrc.cjs create mode 100644 src/frontend/user-frontend/Dockerfile create mode 100644 src/frontend/user-frontend/README.md create mode 100644 src/frontend/user-frontend/components.d.ts create mode 100644 src/frontend/user-frontend/env.d.ts create mode 100644 src/frontend/user-frontend/index.html create mode 100644 src/frontend/user-frontend/jsconfig.json create mode 100644 src/frontend/user-frontend/nginx.conf create mode 100644 src/frontend/user-frontend/package-lock.json create mode 100644 src/frontend/user-frontend/package.json create mode 100644 src/frontend/user-frontend/src/App.vue create mode 100644 src/frontend/user-frontend/src/assets/base.css create mode 100644 src/frontend/user-frontend/src/assets/logo.ico create mode 100644 src/frontend/user-frontend/src/assets/logo.svg create mode 100644 src/frontend/user-frontend/src/assets/main.css create mode 100644 src/frontend/user-frontend/src/assets/project_goals.png create mode 100644 src/frontend/user-frontend/src/components/HomePage.vue create mode 100644 src/frontend/user-frontend/src/components/Profile.vue create mode 100644 src/frontend/user-frontend/src/components/Runner.vue create mode 100644 src/frontend/user-frontend/src/components/Tasks.vue create mode 100644 src/frontend/user-frontend/src/components/Themes.vue create mode 100644 src/frontend/user-frontend/src/components/WelcomeItem.vue create mode 100644 src/frontend/user-frontend/src/components/icons/IconCommunity.vue create mode 100644 src/frontend/user-frontend/src/components/icons/IconDocumentation.vue create mode 100644 src/frontend/user-frontend/src/components/icons/IconEcosystem.vue create mode 100644 src/frontend/user-frontend/src/components/icons/IconSupport.vue create mode 100644 src/frontend/user-frontend/src/components/icons/IconTooling.vue create mode 100644 src/frontend/user-frontend/src/main.ts create mode 100644 src/frontend/user-frontend/src/plugins/axios.js create mode 100644 src/frontend/user-frontend/src/plugins/index.js create mode 100644 src/frontend/user-frontend/src/plugins/keycloak.js create mode 100644 src/frontend/user-frontend/src/plugins/router.js create mode 100644 src/frontend/user-frontend/src/router/index.ts create mode 100644 src/frontend/user-frontend/src/views/AboutView.vue create mode 100644 src/frontend/user-frontend/src/views/ErrorView.vue create mode 100644 src/frontend/user-frontend/src/views/HomeView.vue create mode 100644 src/frontend/user-frontend/tsconfig.app.json create mode 100644 src/frontend/user-frontend/tsconfig.json create mode 100644 src/frontend/user-frontend/tsconfig.node.json create mode 100644 src/frontend/user-frontend/typed-router.d.ts create mode 100644 src/frontend/user-frontend/vite.config.mjs create mode 100644 src/frontend/user-frontend/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2546fd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea + +**/target + +**/node_modules \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..48d62b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] Dzyk Anton + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..4fdcc9d --- /dev/null +++ b/Readme.md @@ -0,0 +1,52 @@ +Базовые компоненты обучающего портала DevOps-практик +==================================================== + +Данный проект содержит базовые компоненты для построения обучающего портала DevOps-практикам. + +Стек +---- + +1. API Server + * Java 21, Spring Boot 3 +2. Engine (Docker) + * Golang 1.21 +3. Frontend + * Vue.js, axios + + +Связанные сервисы +----------------- + +1. Keycloak +2. PostgreSQL +3. Kafka + + +Сборка +------ + +```shell +cd src/backend/api-server +docker build -t api-server . +``` + +```shell +cd src/backend/engines/docker +docker build -t docker-engine . +``` + +```shell +cd src/frontend/user-frontend +docker build -t user-frontend . +``` + +Локальная разработка +-------------------- + +Для разработки необходимо [запустить](./debug/Readme.md) связанные сервисы. + + +Лицензия +-------- + +Данный проект распространяется под лицензией [Apache 2.0](./LICENSE) \ No newline at end of file diff --git a/debug/.env b/debug/.env new file mode 100644 index 0000000..2433c54 --- /dev/null +++ b/debug/.env @@ -0,0 +1,3 @@ +KEYCLOAK_VERSION=24.0.5 +KAFKA_IMAGE_VERSION=7.7.1 +POSTGRESQL_VERSION=16 \ No newline at end of file diff --git a/debug/Readme.md b/debug/Readme.md new file mode 100644 index 0000000..72d8775 --- /dev/null +++ b/debug/Readme.md @@ -0,0 +1,14 @@ +Запуск окружения для разработки +=============================== + + +```shell +docker compose up -d +``` + +Пересоздание стенда +```shell +docker compose down +docker volume rm education_system +docker compose up -d +``` \ No newline at end of file diff --git a/debug/configs/application.properties b/debug/configs/application.properties new file mode 100644 index 0000000..55181bd --- /dev/null +++ b/debug/configs/application.properties @@ -0,0 +1,3 @@ +spring.datasource.url=jdbc:postgresql://localhost:5432/education_system +spring.datasource.username=postgres +spring.datasource.password=postgres \ No newline at end of file diff --git a/debug/configs/init-db.sh b/debug/configs/init-db.sh new file mode 100755 index 0000000..232d576 --- /dev/null +++ b/debug/configs/init-db.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +# Keycloak +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER keycloak PASSWORD 'keycloak'; + CREATE DATABASE keycloak OWNER keycloak; +EOSQL + +# API Server +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER education_system PASSWORD 'education_system'; + CREATE DATABASE education_system OWNER education_system; +EOSQL diff --git a/debug/docker-compose.yml b/debug/docker-compose.yml new file mode 100644 index 0000000..dbc2469 --- /dev/null +++ b/debug/docker-compose.yml @@ -0,0 +1,102 @@ +name: education-system + +services: + postgres: + container_name: edu_postgres + image: postgres:${POSTGRESQL_VERSION} + restart: always + volumes: + - education_system_postgres:/var/lib/postgresql/data + - ./configs/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh + ports: + - "5432:5432" + networks: + - education_system + environment: + POSTGRES_PASSWORD: postgres + + keycloak: + container_name: edu_keycloak + restart: on-failure:5 + image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} + environment: + KC_LOG_LEVEL: debug + KC_DB: postgres + KC_DB_URL: 'jdbc:postgresql://postgres:5432/keycloak' + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: keycloak + KC_DB_SCHEMA: public + KC_HOSTNAME: portal.local + KC_HOSTNAME_PATH: keycloak + KC_HTTP_RELATIVE_PATH: keycloak + KC_HTTP_ENABLED: true + HTTP_ADDRESS_FORWARDING: true + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + ports: + - '8080:8080' + command: start-dev + networks: + - education_system + depends_on: + - postgres + + zookeeper: + image: confluentinc/cp-zookeeper:${KAFKA_IMAGE_VERSION} + container_name: edu_zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + + kafka: + image: confluentinc/cp-kafka:${KAFKA_IMAGE_VERSION} + container_name: edu_kafka + depends_on: + - zookeeper + ports: + - "29092:29092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + + init-kafka: + container_name: edu_init_kafka + image: confluentinc/cp-kafka:${KAFKA_IMAGE_VERSION} + depends_on: + - kafka + entrypoint: [ '/bin/sh', '-c' ] + command: | + " + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic edu_task_docker --replication-factor 1 --partitions 1 + kafka-topics --bootstrap-server kafka:9092 --create --if-not-exists --topic edu_results --replication-factor 1 --partitions 1 + + kafka-topics --bootstrap-server kafka:9092 --list + " + + kafka-ui: + container_name: edu_kafka_ui + image: provectuslabs/kafka-ui:v0.7.2 + ports: + - 8001:8080 + depends_on: + - kafka + - zookeeper + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 + KAFKA_CLUSTERS_0_METRICS_PORT: 9997 + +volumes: + education_system_postgres: + name: education_system + driver: local + +networks: + education_system: + name: education-system \ No newline at end of file diff --git a/src/backend/api-server/.gitignore b/src/backend/api-server/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/src/backend/api-server/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/src/backend/api-server/.mvn/wrapper/maven-wrapper.properties b/src/backend/api-server/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/src/backend/api-server/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/src/backend/api-server/mvnw b/src/backend/api-server/mvnw new file mode 100755 index 0000000..d7c358e --- /dev/null +++ b/src/backend/api-server/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/src/backend/api-server/mvnw.cmd b/src/backend/api-server/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/src/backend/api-server/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/src/backend/api-server/pom.xml b/src/backend/api-server/pom.xml new file mode 100644 index 0000000..795f9f2 --- /dev/null +++ b/src/backend/api-server/pom.xml @@ -0,0 +1,181 @@ + + + 4.0.0 + + ru.oa2.edu + edu-api-server + 0.0.1-SNAPSHOT + ApiServer + Education System + + + 21 + ${java.version} + ${java.version} + + 3.3.3 + 6.1.12 + 6.3.3 + 24.0.5 + 3.2.3 + + 42.7.2 + 2.1.1 + 3.1.0 + 1.18.34 + 5.10.3 + + 3.13.0 + + + + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot.version} + + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring-boot.version} + + + + org.springframework.boot + spring-boot-starter-security + ${spring-boot.version} + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + ${spring-boot.version} + + + + org.keycloak + keycloak-spring-security-adapter + ${keycloak.version} + + + + org.postgresql + postgresql + ${postgresql.version} + + + + jakarta.annotation + jakarta.annotation-api + ${jakarta.annotation-api.version} + + + + jakarta.validation + jakarta.validation-api + ${jakarta.validation-api.version} + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.springframework.boot + spring-boot-starter-undertow + ${spring-boot.version} + + + + org.springframework.kafka + spring-kafka + ${spring-kafka.version} + + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + + + + org.projectlombok + lombok + ${lombok.version} + true + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + test + + + + org.springframework + spring-web + ${springframework.version} + + + + org.springframework.security + spring-security-test + ${spring-security-test.version} + test + + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + + org.projectlombok + lombok + ${lombok.version} + + + true + + + + + + diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/ApiServerApplication.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/ApiServerApplication.java new file mode 100644 index 0000000..f3da98d --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/ApiServerApplication.java @@ -0,0 +1,24 @@ +package ru.oa2.edu.api.application; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@SpringBootApplication +@ComponentScan(basePackages={"ru.oa2.edu.api.*"}) +@EnableJpaRepositories(basePackages={ + "ru.oa2.edu.api.application.database", + "ru.oa2.edu.api.domain.job" +}) +@EnableTransactionManagement +@EntityScan(basePackages="ru.oa2.edu.api.application.entities") +public class ApiServerApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiServerApplication.class, args); + } + +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/ApplicationConfig.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/ApplicationConfig.java new file mode 100644 index 0000000..dd5b30a --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/ApplicationConfig.java @@ -0,0 +1,15 @@ +package ru.oa2.edu.api.application.config; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.security.authorization.AuthorizationEventPublisher; +import org.springframework.security.authorization.SpringAuthorizationEventPublisher; + +public class ApplicationConfig { + + @Bean + public AuthorizationEventPublisher authorizationEventPublisher + (ApplicationEventPublisher applicationEventPublisher) { + return new SpringAuthorizationEventPublisher(applicationEventPublisher); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/CustomSuccessHandler.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/CustomSuccessHandler.java new file mode 100644 index 0000000..fd21695 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/CustomSuccessHandler.java @@ -0,0 +1,34 @@ +package ru.oa2.edu.api.application.config; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; + + +import java.io.IOException; + +@Slf4j +public class CustomSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + @Override + public void onAuthenticationSuccess(final HttpServletRequest request, + final HttpServletResponse response, final Authentication authentication) + throws IOException, ServletException { + super.onAuthenticationSuccess(request, response, authentication); + + HttpSession session = request.getSession(true); + + try { + var jwtAuth = ((JwtAuthenticationToken) authentication).getTokenAttributes(); + var sub = (String) jwtAuth.get("sub"); + log.info(String.format("SUB: %s", sub)); + } catch (Exception e) { + log.error("Error in getting User info", e); + } + } + +} \ No newline at end of file diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/KafkaConfig.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/KafkaConfig.java new file mode 100644 index 0000000..eed8053 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/KafkaConfig.java @@ -0,0 +1,70 @@ +package ru.oa2.edu.api.application.config; + + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.*; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaConfig { + + @Value("${kafka.address}") + private String kafkaAddress; + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaAddress); + configProps.put( + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, + StringSerializer.class); + configProps.put( + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, + StringSerializer.class); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public ConsumerFactory consumerFactory() { + Map props = new HashMap<>(); + props.put( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaAddress); + props.put( + ConsumerConfig.GROUP_ID_CONFIG, + "edu_results"); + props.put( + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + StringDeserializer.class); + props.put( + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + StringDeserializer.class); + return new DefaultKafkaConsumerFactory<>(props); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory + kafkaListenerContainerFactory() { + + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/SaveUser.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/SaveUser.java new file mode 100644 index 0000000..79da837 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/SaveUser.java @@ -0,0 +1,35 @@ +package ru.oa2.edu.api.application.config; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationEventPublisher; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import ru.oa2.edu.api.application.database.UserJpaRepository; +import ru.oa2.edu.api.domain.user.User; + +import java.io.IOException; +import java.util.function.Supplier; + +@Slf4j +@Component +public class SaveUser implements AuthorizationEventPublisher { + + @Autowired + UserJpaRepository userRepository; + + @Override + public void publishAuthorizationEvent(Supplier authentication, T object, AuthorizationDecision decision) { + if (userRepository.findByInternalId(authentication.get().getName()) == null) { + userRepository.save(new User(authentication.get().getName())); + log.info("User with name {} was added", authentication.get().getName()); + } + } +} \ No newline at end of file diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/SecurityConfig.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/SecurityConfig.java new file mode 100644 index 0000000..e2858c6 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/config/SecurityConfig.java @@ -0,0 +1,34 @@ +package ru.oa2.edu.api.application.config; + +import jakarta.servlet.http.HttpServletRequest; +import org.keycloak.adapters.springsecurity.account.SimpleKeycloakAccount; +import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; +import org.keycloak.representations.AccessToken; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .requestMatchers("/actuator/**").permitAll() + .anyRequest().authenticated() + ) + .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults())); + return http.build(); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/ApiController.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/ApiController.java new file mode 100644 index 0000000..9a218dd --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/ApiController.java @@ -0,0 +1,45 @@ +package ru.oa2.edu.api.application.controller; + +import java.util.Collection; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Role; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import ru.oa2.edu.api.domain.task.TaskDTO; +import ru.oa2.edu.api.domain.theme.ThemeDTO; +import ru.oa2.edu.api.application.services.RepositoryService; + +@RestController +@RequestMapping("/api") +public class ApiController { + + private final RepositoryService repositoryService; + + public ApiController(RepositoryService repositoryService) { + this.repositoryService = repositoryService; + } + + @GetMapping() + @RequestMapping("/themes") + @PreAuthorize("hasAnyRole('USER', 'ADMIN', 'CURATOR')") + public Collection getThemes() { + return repositoryService.listThemes(); + } + + + + @GetMapping() + @RequestMapping("/task/{id}") + @PreAuthorize("hasAnyRole('USER', 'ADMIN', 'CURATOR')") + public TaskDTO getTask(@PathVariable("id") long id) { + return repositoryService.getTask(id); + } + + @GetMapping("/version") + @PreAuthorize("hasAnyRole('USER', 'ADMIN', 'CURATOR')") + public String getVersion() { + return "1.0.0"; + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/RunController.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/RunController.java new file mode 100644 index 0000000..6dbe378 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/RunController.java @@ -0,0 +1,40 @@ +package ru.oa2.edu.api.application.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.oa2.edu.api.domain.dto.EngineTypes; +import ru.oa2.edu.api.domain.dto.Job; +import ru.oa2.edu.api.application.services.RunnerService; + +@RestController +@RequestMapping("/api") +public class RunController { + + @Autowired + RunnerService runnerService; + + @PutMapping() + @RequestMapping("/run") + public ResponseEntity run(@RequestBody Job job) { + EngineTypes.DOCKER.toString(); + switch (EngineTypes.fromString(job.getEngineType())) { + case EngineTypes.DOCKER, EngineTypes.DOCKER_BUILDX: { + var runId = runnerService.Job("edu_task_docker", job); + return ResponseEntity.ok().body(runId); + } + default: { + var res = ResponseEntity.badRequest(); + res.body("not support engine"); + return res.build(); + } + } + } + + @GetMapping() + @RequestMapping("/run/status/{job_id}") + public ResponseEntity getResult(@PathVariable("job_id") long jobId) { + var result = runnerService.Status(jobId); + return ResponseEntity.ok().body(result); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/ThemeController.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/ThemeController.java new file mode 100644 index 0000000..94c154a --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/ThemeController.java @@ -0,0 +1,37 @@ +package ru.oa2.edu.api.application.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import ru.oa2.edu.api.application.services.RepositoryService; +import ru.oa2.edu.api.domain.theme.ThemeDTO; + +@RestController +@RequestMapping("/api/theme") +public class ThemeController { + + private final RepositoryService repositoryService; + + public ThemeController(RepositoryService repositoryService) { + this.repositoryService = repositoryService; + } + + @GetMapping("{id}") + @PreAuthorize("hasAnyRole('USER', 'ADMIN', 'CURATOR')") + public ThemeDTO getTheme(@PathVariable("id") Long id) { + return repositoryService.getTheme(id); + } + + @PostMapping + @PreAuthorize("hasAnyRole('CURATOR', 'ADMIN')") + public ResponseEntity createTheme(@RequestBody ThemeDTO themeDTO) { + repositoryService.createTheme(themeDTO); + return ResponseEntity.ok().build(); + } + + @PutMapping("{id}") + @PreAuthorize("hasAnyRole('CURATOR', 'ADMIN')") + public ThemeDTO updateTheme(@PathVariable("id") Long id, @RequestBody ThemeDTO themeDTO) { + return repositoryService.updateTheme(id, themeDTO); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/UserController.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/UserController.java new file mode 100644 index 0000000..5eaff5a --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/controller/UserController.java @@ -0,0 +1,37 @@ +package ru.oa2.edu.api.application.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.oa2.edu.api.application.services.RepositoryService; +import ru.oa2.edu.api.domain.user.UserInfo; +import ru.oa2.edu.api.domain.user.UserRepository; +import ru.oa2.edu.api.domain.user.UserStatistic; + +@RestController +@RequestMapping("/api/user") +public class UserController { + + @Autowired + RepositoryService repositoryService; + + @Autowired + UserRepository userRepository; + + @GetMapping("/current") + public UserInfo getCurrentUser(Authentication authentication) { + var jwtAuth = ((JwtAuthenticationToken) authentication).getTokenAttributes(); + + var user = userRepository.findByInternalId(authentication.getName()); + return new UserInfo( + authentication.getName(), + jwtAuth.get("family_name").toString(), + jwtAuth.get("given_name").toString(), + jwtAuth.get("email").toString(), + repositoryService.getUserStatistic(user.getId()) + ); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/JobJpaRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/JobJpaRepository.java new file mode 100644 index 0000000..e31e504 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/JobJpaRepository.java @@ -0,0 +1,10 @@ +package ru.oa2.edu.api.application.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.oa2.edu.api.domain.job.Job; +import ru.oa2.edu.api.domain.job.JobRepository; + +@Repository +public interface JobJpaRepository extends JpaRepository, JobRepository { +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/TaskJpaRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/TaskJpaRepository.java new file mode 100644 index 0000000..5da255e --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/TaskJpaRepository.java @@ -0,0 +1,10 @@ +package ru.oa2.edu.api.application.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.oa2.edu.api.domain.task.Task; +import ru.oa2.edu.api.domain.task.TaskRepository; + +@Repository +public interface TaskJpaRepository extends JpaRepository, TaskRepository { +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/ThemeJpaRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/ThemeJpaRepository.java new file mode 100644 index 0000000..186ac7a --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/ThemeJpaRepository.java @@ -0,0 +1,10 @@ +package ru.oa2.edu.api.application.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.oa2.edu.api.domain.theme.Theme; +import ru.oa2.edu.api.domain.theme.ThemeRepository; + +@Repository +public interface ThemeJpaRepository extends JpaRepository, ThemeRepository { +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/UserJpaRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/UserJpaRepository.java new file mode 100644 index 0000000..5627fd2 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/UserJpaRepository.java @@ -0,0 +1,10 @@ +package ru.oa2.edu.api.application.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.oa2.edu.api.domain.user.User; +import ru.oa2.edu.api.domain.user.UserRepository; + +@Repository +public interface UserJpaRepository extends JpaRepository, UserRepository { +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/UserProgressJpaRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/UserProgressJpaRepository.java new file mode 100644 index 0000000..43fbb22 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/database/UserProgressJpaRepository.java @@ -0,0 +1,8 @@ +package ru.oa2.edu.api.application.database; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.oa2.edu.api.domain.user.UserProgress; +import ru.oa2.edu.api.domain.user.UserProgressRepository; + +public interface UserProgressJpaRepository extends JpaRepository, UserProgressRepository { +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RepositoryService.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RepositoryService.java new file mode 100644 index 0000000..5c62e82 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RepositoryService.java @@ -0,0 +1,141 @@ +package ru.oa2.edu.api.application.services; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.oa2.edu.api.application.database.*; +import ru.oa2.edu.api.application.utils.TaskComparator; +import ru.oa2.edu.api.application.utils.ThemeComparator; +import ru.oa2.edu.api.domain.task.TaskDTO; +import ru.oa2.edu.api.domain.theme.Theme; +import ru.oa2.edu.api.domain.theme.ThemeDTO; +import ru.oa2.edu.api.domain.user.StatJob; +import ru.oa2.edu.api.domain.user.UserDataStatistic; + +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +@Transactional +public class RepositoryService { + + private final ThemeJpaRepository themeRepository; + private final TaskJpaRepository taskRepository; + private final UserJpaRepository userRepository; + private final UserProgressJpaRepository userProgressRepository; + private final JobJpaRepository jobJpaRepository; + + public RepositoryService(ThemeJpaRepository themeRepository, + TaskJpaRepository taskRepository, + UserJpaRepository userRepository, + UserProgressJpaRepository userProgressRepository, JobJpaRepository jobJpaRepository) { + this.themeRepository = themeRepository; + this.taskRepository = taskRepository; + this.userRepository = userRepository; + this.userProgressRepository = userProgressRepository; + this.jobJpaRepository = jobJpaRepository; + } + + public Collection listThemes() { + var themes = themeRepository.findAll(); + var list = new ArrayList(); + for (var theme : themes) { + list.add( + ThemeDTO.builder() + .id(theme.getId()) + .name(theme.getName()) + .active(theme.isActive()) + .created(theme.getCreated()) + .updated(theme.getUpdated()) + .tasks(null) + .build() + ); + } + list.sort(new ThemeComparator()); + return list; + } + + public ThemeDTO getTheme(Long id) { + ThemeDTO themeDTO = null; + var result = themeRepository.findById(id); + if (result.isPresent()) { + var theme = result.get(); + Collection tasks = new ArrayList<>(); + for (var task : theme.getTasks()) { + tasks.add( + TaskDTO.builder() + .id(task.getId()) + .name(task.getName()) + .active(task.isActive()) + .data(task.getData()) + .created(task.getCreated()) + .updated(task.getUpdated()) + .build() + ); + } + tasks = tasks.stream().sorted(new TaskComparator()).toList(); + themeDTO = ThemeDTO.builder() + .id(theme.getId()) + .name(theme.getName()) + .active(theme.isActive()) + .created(theme.getCreated()) + .updated(theme.getUpdated()) + .tasks(tasks) + .build(); + } + return themeDTO; + } + + public TaskDTO getTask(long id) { + var task = taskRepository.findById(id); + return TaskDTO.builder() + .id(task.getId()) + .name(task.getName()) + .data(task.getData()) + .active(task.isActive()) + .created(task.getCreated()) + .updated(task.getUpdated()) + .build(); + } + + public UserDataStatistic getUserStatistic(long id) { + var stats = new HashMap(); + var stats2 = new HashMap(); + var userData = new UserDataStatistic(); + var result = jobJpaRepository.findUserStatistic(id); + + for (var job : result) { + var date = job.getLastDate().toLocalDateTime().toLocalDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + var isDone = job.isResult() ? 1 : 0; + if (stats.containsKey(date)) { + stats.put(date, stats.get(date) + 1); + stats2.put(date, stats2.get(date) + isDone); + } else { + stats.put(date, 1); + stats2.put(date, isDone); + } + } + for (var stat : stats.entrySet()) { + userData.addLabel(stat.getKey()); + userData.addTotal(stat.getValue()); + } + for (var stat : stats2.entrySet()) { + userData.addFinishedCount(stat.getValue()); + } + return userData; + } + + public void createTheme(ThemeDTO themeDTO) { + var theme = new Theme(themeDTO.getName(), themeDTO.isActive()); + themeRepository.save(theme); + } + + public ThemeDTO updateTheme(Long id, ThemeDTO themeDTO) { + // TODO + return null; + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RunnerService.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RunnerService.java new file mode 100644 index 0000000..7417d30 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RunnerService.java @@ -0,0 +1,10 @@ +package ru.oa2.edu.api.application.services; + +import ru.oa2.edu.api.domain.dto.Job; +import ru.oa2.edu.api.domain.dto.JobResult; + +public interface RunnerService { + + long Job(String topicName, Job job); + JobResult Status(long jobId); +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RunnerServiceImpl.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RunnerServiceImpl.java new file mode 100644 index 0000000..f3dc5b0 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/services/RunnerServiceImpl.java @@ -0,0 +1,132 @@ +package ru.oa2.edu.api.application.services; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import jakarta.transaction.Transactional; +import lombok.extern.log4j.Log4j2; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import ru.oa2.edu.api.application.database.JobJpaRepository; +import ru.oa2.edu.api.application.database.TaskJpaRepository; +import ru.oa2.edu.api.application.database.ThemeJpaRepository; +import ru.oa2.edu.api.application.database.UserJpaRepository; +import ru.oa2.edu.api.domain.dto.DataCase; +import ru.oa2.edu.api.domain.dto.JobResult; +import ru.oa2.edu.api.domain.dto.JobStatus; +import ru.oa2.edu.api.domain.job.Job; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.UUID; + +import static ru.oa2.edu.api.application.utils.MappingTypes.marshalRequestData; + +@Log4j2 +@Service +@Transactional +public class RunnerServiceImpl implements RunnerService { + + private final KafkaTemplate kafkaTemplate; + private final UserJpaRepository userRepository; + private final TaskJpaRepository taskRepository; + private final JobJpaRepository jobRepository; + private final ThemeJpaRepository themeRepository; + + public RunnerServiceImpl(KafkaTemplate kafkaTemplate, + UserJpaRepository userRepository, + TaskJpaRepository taskRepository, + JobJpaRepository jobRepository, + ThemeJpaRepository themeRepository) { + this.kafkaTemplate = kafkaTemplate; + this.userRepository = userRepository; + this.taskRepository = taskRepository; + this.jobRepository = jobRepository; + this.themeRepository = themeRepository; + } + + @Override + public long Job(String topicName, ru.oa2.edu.api.domain.dto.Job jobDTO) { + var mapperObject = new ObjectMapper(); + var user = userRepository.findById(jobDTO.getUserId()); + var task = taskRepository.findById(jobDTO.getTaskId()); + + String requestDataString = null; + try { + requestDataString = marshalRequestData(jobDTO.getRequestData()); + } catch (Exception e) { + log.error(e); + } + var job = new Job(user, task, JobStatus.RUNNING, requestDataString); + jobRepository.save(job); + + var uuid = UUID.randomUUID(); + var requestData = jobDTO.getRequestData(); + var data = task.getData(); + DataCase dataCase = null; + try { + dataCase = mapperObject.readValue(data, DataCase.class); + } catch (JsonProcessingException e) { + //TODO + } + requestData.setContextPath(dataCase.getRequestData().getContextPath()); + var jobResult = new ru.oa2.edu.api.domain.dto.Job( + job.getId(), + job.getUser().getId(), + job.getTask().getId(), + jobDTO.getEngineType(), + requestData, + dataCase.getTestCases() + ); + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String json = null; + try { + json = ow.writeValueAsString(jobResult); + } catch (JsonProcessingException e) { + // TODO + } + kafkaTemplate.send(topicName, uuid.toString(), json); + // TODO journal message + + return job.getId(); + } + + @Override + public JobResult Status(long jobId) { + var res = jobRepository.findById(jobId); + if (res.isPresent()) { + var job = res.get(); + if (job.getStatus().equals(JobStatus.RUNNING)) { + return null; + } + return new JobResult( + jobId, + job.getTask().getId(), + job.isResult(), + job.getTaskResponse() != null ? Arrays.stream(job.getTaskResponse().split("\n")).toList() : new ArrayList<>(), + null); + } + return null; + } + + @KafkaListener(topics = "edu_results", groupId = "edu") + public void listenGroupFoo(String message) { + System.out.println("Received Message in group foo: " + message); + ObjectMapper mapper = new ObjectMapper(); + JobResult jobResult = null; + try { + jobResult = mapper.readValue(message, JobResult.class); + } catch (JsonProcessingException e) { + log.error(e); + return; + } + var res = jobRepository.findById(jobResult.getJobId()); + // TODO + var job = res.get(); + job.updateResult(jobResult.isStatus(), String.join("\n", jobResult.getLog())); + jobRepository.save(job); + // TODO как аккумулировать статистику (в процессе запроса или при обработке) + // TODO journal message + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/MappingTypes.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/MappingTypes.java new file mode 100644 index 0000000..51d5180 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/MappingTypes.java @@ -0,0 +1,18 @@ +package ru.oa2.edu.api.application.utils; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import ru.oa2.edu.api.domain.dto.RequestData; + +public class MappingTypes { + + final static ObjectMapper objectMapper = new ObjectMapper(); + + public static RequestData parseRequestData(Object object) throws Exception { + return objectMapper.readValue((JsonParser) object, RequestData.class); + } + + public static String marshalRequestData(RequestData requestData) throws Exception { + return objectMapper.writeValueAsString(requestData); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/TaskComparator.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/TaskComparator.java new file mode 100644 index 0000000..e4023c0 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/TaskComparator.java @@ -0,0 +1,12 @@ +package ru.oa2.edu.api.application.utils; + +import ru.oa2.edu.api.domain.task.TaskDTO; + +import java.util.Comparator; + +public class TaskComparator implements Comparator { + @Override + public int compare(TaskDTO t1, TaskDTO t2) { + return Long.compare(t1.getId(), t2.getId()); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/ThemeComparator.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/ThemeComparator.java new file mode 100644 index 0000000..c56e70b --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/application/utils/ThemeComparator.java @@ -0,0 +1,12 @@ +package ru.oa2.edu.api.application.utils; + +import ru.oa2.edu.api.domain.theme.ThemeDTO; + +import java.util.Comparator; + +public class ThemeComparator implements Comparator { + @Override + public int compare(ThemeDTO t1, ThemeDTO t2) { + return Long.compare(t1.getId(), t2.getId()); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/DataCase.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/DataCase.java new file mode 100644 index 0000000..9b81293 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/DataCase.java @@ -0,0 +1,34 @@ +package ru.oa2.edu.api.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +public class DataCase { + + public DataCase() {} + + @JsonProperty("user_id") + private long userId; + + @JsonProperty("description") + private String description; + + @JsonProperty("task_id") + private long taskId; + + @JsonProperty("engine_type") + private String engineType; + + @JsonProperty("request_data") + private RequestData requestData; + + @JsonProperty("test_cases") + private List testCases; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/EngineTypes.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/EngineTypes.java new file mode 100644 index 0000000..567c7c4 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/EngineTypes.java @@ -0,0 +1,25 @@ +package ru.oa2.edu.api.domain.dto; + +public enum EngineTypes { + DOCKER ("docker"), + DOCKER_BUILDX ("docker_buildx"), + KUBERNETES ("kubernetes"); + + private final String name; + private EngineTypes(String name) { + this.name = name; + } + + public String toString() { + return this.name; + } + + public static EngineTypes fromString(String text) { + for (EngineTypes t : EngineTypes.values()) { + if (t.name.equalsIgnoreCase(text)) { + return t; + } + } + return null; + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/Job.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/Job.java new file mode 100644 index 0000000..095a959 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/Job.java @@ -0,0 +1,32 @@ +package ru.oa2.edu.api.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +public class Job { + + @JsonProperty("job_id") + private long jobId; + + @JsonProperty("user_id") + private long userId; + + @JsonProperty("task_id") + private long taskId; + + @JsonProperty("engine_type") + private String engineType; + + @JsonProperty("request_data") + private RequestData requestData; + + @JsonProperty("test_cases") + private List testCases; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/JobResult.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/JobResult.java new file mode 100644 index 0000000..a44bfa3 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/JobResult.java @@ -0,0 +1,29 @@ +package ru.oa2.edu.api.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +public class JobResult { + + public JobResult() {} + + @JsonProperty("job_id") + private long jobId; + @JsonProperty("task_id") + private long taskId; + @JsonProperty("status") + private boolean status; + + @JsonProperty("log") + private List log; + + @JsonProperty("result") + private String result; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/JobStatus.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/JobStatus.java new file mode 100644 index 0000000..e46c215 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/JobStatus.java @@ -0,0 +1,6 @@ +package ru.oa2.edu.api.domain.dto; + +public enum JobStatus { + RUNNING, + FINISHED, +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/RequestData.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/RequestData.java new file mode 100644 index 0000000..e9cccb6 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/RequestData.java @@ -0,0 +1,19 @@ +package ru.oa2.edu.api.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class RequestData { + + public RequestData() {} + + @JsonProperty("main") + private String main; + @JsonProperty("context_path") + private String contextPath; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/Run.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/Run.java new file mode 100644 index 0000000..b768984 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/Run.java @@ -0,0 +1,19 @@ +package ru.oa2.edu.api.domain.dto; + +import lombok.Builder; +import lombok.Getter; + +import java.sql.Timestamp; + +@Getter +@Builder +public class Run { + private long id; + private long userId; + private long taskId; + private JobStatus status; + private Object taskRequest; + private String taskResponse; + private boolean result; + private Timestamp lastDate; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/TestCase.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/TestCase.java new file mode 100644 index 0000000..dc64305 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/TestCase.java @@ -0,0 +1,21 @@ +package ru.oa2.edu.api.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class TestCase { + + public TestCase() {} + + @JsonProperty("case_id") + private long caseId; + @JsonProperty("case_type") + private String caseType; + @JsonProperty("context") + private Object Context; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/UserProgress.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/UserProgress.java new file mode 100644 index 0000000..d65723e --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/dto/UserProgress.java @@ -0,0 +1,30 @@ +package ru.oa2.edu.api.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Builder +@AllArgsConstructor +public class UserProgress { + + public UserProgress() {} + + @JsonProperty("id") + private Long id; + + @JsonProperty("user_id") + private Long userId; + @JsonProperty("theme_id") + private Long themeId; + @JsonProperty("task_id") + private Long taskId; + @JsonProperty("count") + private Long count; + @JsonProperty("done") + private boolean done; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/Job.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/Job.java new file mode 100644 index 0000000..74142e9 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/Job.java @@ -0,0 +1,110 @@ +package ru.oa2.edu.api.domain.job; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; +import ru.oa2.edu.api.domain.dto.JobStatus; +import ru.oa2.edu.api.domain.dto.RequestData; +import ru.oa2.edu.api.domain.task.Task; +import ru.oa2.edu.api.domain.user.User; + +import java.sql.Timestamp; + +import static ru.oa2.edu.api.application.utils.MappingTypes.parseRequestData; + + +public class Job { + + + private long id; + private User user; + private Task task; + private JobStatus status; + private String taskRequest; + + private String taskResponse; + private boolean result; + private Timestamp lastDate; + + public Job() { + + } + + public Job(User user, Task task, JobStatus status, String taskRequest) { + this.user = user; + this.task = task; + this.status = status; + this.taskRequest = taskRequest; + this.lastDate = new Timestamp(System.currentTimeMillis()); + } + + public void updateResult(boolean result, String taskResponse) { + this.result = result; + this.status = JobStatus.FINISHED; + this.taskResponse = taskResponse; + this.lastDate = new Timestamp(System.currentTimeMillis()); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Task getTask() { + return task; + } + + public void setTask(Task task) { + this.task = task; + } + + public JobStatus getStatus() { + return status; + } + + public void setStatus(JobStatus status) { + this.status = status; + } + + public String getTaskRequest() { + return taskRequest; + } + + public void setTaskRequest(String taskRequest) { + this.taskRequest = taskRequest; + } + + public String getTaskResponse() { + return taskResponse; + } + + public void setTaskResponse(String taskResponse) { + this.taskResponse = taskResponse; + } + + public boolean isResult() { + return result; + } + + public void setResult(boolean result) { + this.result = result; + } + + public Timestamp getLastDate() { + return lastDate; + } + + public void setLastDate(Timestamp lastDate) { + this.lastDate = lastDate; + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/JobRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/JobRepository.java new file mode 100644 index 0000000..9343ae7 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/JobRepository.java @@ -0,0 +1,8 @@ +package ru.oa2.edu.api.domain.job; + +import java.util.List; + +public interface JobRepository { + + List findUserStatistic(long userId); +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/JobRepositoryImpl.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/JobRepositoryImpl.java new file mode 100644 index 0000000..4075305 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/job/JobRepositoryImpl.java @@ -0,0 +1,21 @@ +package ru.oa2.edu.api.domain.job; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class JobRepositoryImpl implements JobRepository { + + @PersistenceContext + private EntityManager entityManager; + + @Override + public List findUserStatistic(long userId) { + var query = entityManager.createQuery("from Job where user.Id = :userId", Job.class); + query.setParameter("userId", userId); + return query.getResultList(); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/Task.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/Task.java new file mode 100644 index 0000000..b2de2b6 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/Task.java @@ -0,0 +1,62 @@ +package ru.oa2.edu.api.domain.task; + +import java.time.LocalDateTime; + + +public class Task { + + private Long id; + private String name; + private String data; + private boolean active; + private LocalDateTime created; + private LocalDateTime updated; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public LocalDateTime getCreated() { + return created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + + public LocalDateTime getUpdated() { + return updated; + } + + public void setUpdated(LocalDateTime updated) { + this.updated = updated; + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/TaskDTO.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/TaskDTO.java new file mode 100644 index 0000000..0190f51 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/TaskDTO.java @@ -0,0 +1,20 @@ +package ru.oa2.edu.api.domain.task; + +import java.time.LocalDateTime; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class TaskDTO { + + private Long id; + private String name; + private String data; + private boolean active; + private LocalDateTime created; + private LocalDateTime updated; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/TaskRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/TaskRepository.java new file mode 100644 index 0000000..a32b14f --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/task/TaskRepository.java @@ -0,0 +1,10 @@ +package ru.oa2.edu.api.domain.task; + +import java.util.List; + +public interface TaskRepository { + + Task save(Task task); + Task findById(long id); + List findAll(); +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/Theme.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/Theme.java new file mode 100644 index 0000000..f76071a --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/Theme.java @@ -0,0 +1,77 @@ +package ru.oa2.edu.api.domain.theme; + + +import ru.oa2.edu.api.domain.task.Task; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + + + +public class Theme { + + private Long id; + private String name; + private boolean active; + private LocalDateTime created; + private LocalDateTime updated; + private Set tasks = new HashSet<>(); + + public Theme() {} + + public Theme(String name, boolean active) { + this.name = name; + this.active = active; + this.created = LocalDateTime.now(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public LocalDateTime getCreated() { + return created; + } + + public void setCreated(LocalDateTime created) { + this.created = created; + } + + public LocalDateTime getUpdated() { + return updated; + } + + public void setUpdated(LocalDateTime updated) { + this.updated = updated; + } + + public Set getTasks() { + return tasks; + } + + public void setTasks(Set tasks) { + this.tasks = tasks; + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/ThemeDTO.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/ThemeDTO.java new file mode 100644 index 0000000..00c5429 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/ThemeDTO.java @@ -0,0 +1,25 @@ +package ru.oa2.edu.api.domain.theme; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import ru.oa2.edu.api.domain.task.TaskDTO; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Set; + +@Getter +@Builder +@AllArgsConstructor +public class ThemeDTO { + + public ThemeDTO() {} + + private Long id; + private String name; + private boolean active; + private LocalDateTime created; + private LocalDateTime updated; + private Collection tasks; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/ThemeRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/ThemeRepository.java new file mode 100644 index 0000000..e34183e --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/theme/ThemeRepository.java @@ -0,0 +1,9 @@ +package ru.oa2.edu.api.domain.theme; + +import java.util.List; + +public interface ThemeRepository { + + Theme findById(long id); + List findAll(); +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/StatJob.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/StatJob.java new file mode 100644 index 0000000..2b5ddff --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/StatJob.java @@ -0,0 +1,13 @@ +package ru.oa2.edu.api.domain.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class StatJob { + private long runs; + private long finish; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/User.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/User.java new file mode 100644 index 0000000..b186a42 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/User.java @@ -0,0 +1,30 @@ +package ru.oa2.edu.api.domain.user; + +public class User { + private Long id; + private String internalId; + + public User() { + + } + + public User(String internalId) { + this.internalId = internalId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getInternalId() { + return internalId; + } + + public void setInternalId(String internalId) { + this.internalId = internalId; + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserDTO.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserDTO.java new file mode 100644 index 0000000..ed096ef --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserDTO.java @@ -0,0 +1,13 @@ +package ru.oa2.edu.api.domain.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class UserDTO { + private String id; + private String internalId; +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserDataStatistic.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserDataStatistic.java new file mode 100644 index 0000000..a78715f --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserDataStatistic.java @@ -0,0 +1,40 @@ +package ru.oa2.edu.api.domain.user; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +@Getter +@Setter +public class UserDataStatistic { + private TreeSet labels; + private TreeSet finishedCount; + private TreeSet total; + private List> series; + + public UserDataStatistic() { + labels = new TreeSet<>(); + total = new TreeSet<>(); + finishedCount = new TreeSet<>(); + } + + public void addLabel(String label) { + labels.add(label); + } + + public void addFinishedCount(int count) { + finishedCount.add(count); + } + + public void addTotal(int count) { + total.add(count); + } + + public List> getSeries() { + return List.of(total, finishedCount); + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserInfo.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserInfo.java new file mode 100644 index 0000000..547fa40 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserInfo.java @@ -0,0 +1,7 @@ +package ru.oa2.edu.api.domain.user; + +import java.util.Map; + +public record UserInfo(String id, String family, String name, String email, UserDataStatistic userStatistic) { + +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserProgress.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserProgress.java new file mode 100644 index 0000000..383b5af --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserProgress.java @@ -0,0 +1,74 @@ +package ru.oa2.edu.api.domain.user; + +import ru.oa2.edu.api.domain.task.Task; +import ru.oa2.edu.api.domain.theme.Theme; + + +public class UserProgress { + + + private Long id; + private User user; + private Theme theme; + private Task task; + private Long count; + private boolean done; + + public UserProgress() {} + + public UserProgress(User user, Theme theme, Task task, Long count, boolean done) { + this.user = user; + this.theme = theme; + this.task = task; + this.count = count; + this.done = done; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Theme getTheme() { + return theme; + } + + public void setTheme(Theme theme) { + this.theme = theme; + } + + public Task getTask() { + return task; + } + + public void setTask(Task task) { + this.task = task; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } + + public boolean isDone() { + return done; + } + + public void setDone(boolean done) { + this.done = done; + } +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserProgressRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserProgressRepository.java new file mode 100644 index 0000000..f57cb19 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserProgressRepository.java @@ -0,0 +1,4 @@ +package ru.oa2.edu.api.domain.user; + +public interface UserProgressRepository { +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserRepository.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserRepository.java new file mode 100644 index 0000000..2af623a --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserRepository.java @@ -0,0 +1,10 @@ +package ru.oa2.edu.api.domain.user; + +import java.util.List; + +public interface UserRepository { + + User findById(long id); + User findByInternalId(String internalId); + List findAll(); +} diff --git a/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserStatistic.java b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserStatistic.java new file mode 100644 index 0000000..bc51bd9 --- /dev/null +++ b/src/backend/api-server/src/main/java/ru/oa2/edu/api/domain/user/UserStatistic.java @@ -0,0 +1,6 @@ +package ru.oa2.edu.api.domain.user; + +import java.util.Map; + +public record UserStatistic(Map stats) { +} diff --git a/src/backend/api-server/src/main/resources/application.yml b/src/backend/api-server/src/main/resources/application.yml new file mode 100644 index 0000000..9c1ce98 --- /dev/null +++ b/src/backend/api-server/src/main/resources/application.yml @@ -0,0 +1,45 @@ +server: + port: 8082 + +spring: + + application: + name: ApiServer + main: + banner-mode: off + + datasource: + url: jdbc:postgresql://localhost:5432/education_system + username: postgres + password: postgres + + jpa: + profiles: + hibernate: + show_sql: true + format_sql: true + hibernate: + ddl-auto: validate + mapping-resources: + - mappings/job.xml + - mappings/task.xml + - mappings/theme.xml + - mappings/internal_user.xml + - mappings/user_progress.xml + + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://portal.local:8080/keycloak/realms/edu-portal + +kafka: + address: "127.0.0.1:29092" + +logging: + level: + org: + hibernate: + orm.jdbc.bind: trace + SQL: debug + diff --git a/src/backend/api-server/src/main/resources/liquibase/Dockerfile b/src/backend/api-server/src/main/resources/liquibase/Dockerfile new file mode 100644 index 0000000..4e5c608 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/Dockerfile @@ -0,0 +1,5 @@ +ARG LIQUIBASE_VERSION=4.9 + +FROM liquibase/liquibase:${LIQUIBASE_VERSION} +ADD src/main/resources /liquibase/changelog +ENTRYPOINT ["/liquibase/docker-entrypoint.sh", "--changeLogFile=db.root-master.xml"] diff --git a/src/backend/api-server/src/main/resources/liquibase/Readme.md b/src/backend/api-server/src/main/resources/liquibase/Readme.md new file mode 100644 index 0000000..7b478f9 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/Readme.md @@ -0,0 +1,33 @@ +База данных +=========== + + +Создание структуры БД +--------------------- + +```shell +docker run \ + --network=education-system \ + --volume $PWD/src/main/resources/db:/liquibase/changelog \ + liquibase/liquibase:4.9 \ + --url="jdbc:postgresql://postgres:5432/education_system" \ + --changelog-file=./changelog/db.root-master.xml \ + --username=postgres \ + --password=postgres \ + update +``` + +Создание тестовых данных +------------------------ + +```shell +docker run \ + --network=education-system \ + --volume $PWD/src/main/resources/demo:/liquibase/changelog \ + liquibase/liquibase:4.9 \ + --url="jdbc:postgresql://postgres:5432/education_system" \ + --changelog-file=./changelog/demo.root-master.xml \ + --username=postgres \ + --password=postgres \ + update +``` diff --git a/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-1-create_theme.xml b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-1-create_theme.xml new file mode 100644 index 0000000..0c77608 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-1-create_theme.xml @@ -0,0 +1,40 @@ + + + + + + + Создание таблицы theme + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-2-create_list.xml b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-2-create_list.xml new file mode 100644 index 0000000..094e64c --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-2-create_list.xml @@ -0,0 +1,26 @@ + + + + + + + Создание таблицы list + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-3-create_task.xml b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-3-create_task.xml new file mode 100644 index 0000000..479207a --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-3-create_task.xml @@ -0,0 +1,44 @@ + + + + + + + Создание таблицы task + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-4-create_internal_user.xml b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-4-create_internal_user.xml new file mode 100644 index 0000000..4d8af58 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-4-create_internal_user.xml @@ -0,0 +1,28 @@ + + + + + + + Создание таблицы user + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-5-create_user_progress.xml b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-5-create_user_progress.xml new file mode 100644 index 0000000..6a93ff1 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240831-5-create_user_progress.xml @@ -0,0 +1,44 @@ + + + + + + + Создание таблицы user_progress + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240919-6-create_job.xml b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240919-6-create_job.xml new file mode 100644 index 0000000..5f2028f --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240919-6-create_job.xml @@ -0,0 +1,52 @@ + + + + + + + Создание таблицы job + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240919-7-create_journal.xml b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240919-7-create_journal.xml new file mode 100644 index 0000000..29f79e5 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.20240919-7-create_journal.xml @@ -0,0 +1,40 @@ + + + + + + + Создание таблицы journal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.version-master.xml b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.version-master.xml new file mode 100644 index 0000000..9e191c2 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/1.0.0/db.version-master.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/db/db.root-master.xml b/src/backend/api-server/src/main/resources/liquibase/db/db.root-master.xml new file mode 100644 index 0000000..c6cf560 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/db/db.root-master.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-1-add_themes.xml b/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-1-add_themes.xml new file mode 100644 index 0000000..cec40ba --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-1-add_themes.xml @@ -0,0 +1,39 @@ + + + + + + + Добавление Тем изучения + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-2-add_tasks.xml b/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-2-add_tasks.xml new file mode 100644 index 0000000..293a7ea --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-2-add_tasks.xml @@ -0,0 +1,151 @@ + + + + + + + Добавление заданий + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-3-add_lists.xml b/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-3-add_lists.xml new file mode 100644 index 0000000..2627252 --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.20240921-3-add_lists.xml @@ -0,0 +1,81 @@ + + + + + + + Связь тем и заданий + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.version-master.xml b/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.version-master.xml new file mode 100644 index 0000000..3293dfa --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/demo/1.0.0/demo.version-master.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/liquibase/demo/demo.root-master.xml b/src/backend/api-server/src/main/resources/liquibase/demo/demo.root-master.xml new file mode 100644 index 0000000..f73e82e --- /dev/null +++ b/src/backend/api-server/src/main/resources/liquibase/demo/demo.root-master.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/mappings/internal_user.xml b/src/backend/api-server/src/main/resources/mappings/internal_user.xml new file mode 100644 index 0000000..7117573 --- /dev/null +++ b/src/backend/api-server/src/main/resources/mappings/internal_user.xml @@ -0,0 +1,24 @@ + + + + ru.oa2.edu.api.domain.theme + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/mappings/job.xml b/src/backend/api-server/src/main/resources/mappings/job.xml new file mode 100644 index 0000000..7ea2acc --- /dev/null +++ b/src/backend/api-server/src/main/resources/mappings/job.xml @@ -0,0 +1,49 @@ + + + + ru.oa2.edu.api.domain.theme + + +
+ + + + + + + + + + STRING + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/mappings/task.xml b/src/backend/api-server/src/main/resources/mappings/task.xml new file mode 100644 index 0000000..fd23375 --- /dev/null +++ b/src/backend/api-server/src/main/resources/mappings/task.xml @@ -0,0 +1,40 @@ + + + + ru.oa2.edu.api.domain.theme + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/mappings/theme.xml b/src/backend/api-server/src/main/resources/mappings/theme.xml new file mode 100644 index 0000000..1c1a5d6 --- /dev/null +++ b/src/backend/api-server/src/main/resources/mappings/theme.xml @@ -0,0 +1,50 @@ + + + + ru.oa2.edu.api.domain.theme + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/main/resources/mappings/user_progress.xml b/src/backend/api-server/src/main/resources/mappings/user_progress.xml new file mode 100644 index 0000000..1e7cc66 --- /dev/null +++ b/src/backend/api-server/src/main/resources/mappings/user_progress.xml @@ -0,0 +1,39 @@ + + + ru.oa2.edu.api.domain.theme + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/backend/api-server/src/test/java/ru/oa2/edu/api/ThemeTest.java b/src/backend/api-server/src/test/java/ru/oa2/edu/api/ThemeTest.java new file mode 100644 index 0000000..6133a1d --- /dev/null +++ b/src/backend/api-server/src/test/java/ru/oa2/edu/api/ThemeTest.java @@ -0,0 +1,23 @@ +package ru.oa2.edu.api; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import ru.oa2.edu.api.application.services.RepositoryService; +import ru.oa2.edu.api.domain.theme.ThemeDTO; + +import java.util.Collection; + +@SpringBootTest +public class ThemeTest { + + @Autowired + private RepositoryService repositoryService; + + @Test + public void getListTest() { + Collection listThemes = repositoryService.listThemes(); + // TODO данные для теста + System.out.println(listThemes); + } +} \ No newline at end of file diff --git a/src/backend/case-services/java-http-ok/pom.xml b/src/backend/case-services/java-http-ok/pom.xml new file mode 100644 index 0000000..78471e5 --- /dev/null +++ b/src/backend/case-services/java-http-ok/pom.xml @@ -0,0 +1,57 @@ + + 4.0.0 + + ru.edu.portal + ok-http-server + 1.0.0 + + Edu Portal Ok Service + + + UTF-8 + 21 + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ru.edu.portal.OkHttpServer + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + ru.edu.portal.OkHttpServer + + + + + + + + \ No newline at end of file diff --git a/src/backend/case-services/java-http-ok/src/main/java/ru/edu/portal/OkHttpServer.java b/src/backend/case-services/java-http-ok/src/main/java/ru/edu/portal/OkHttpServer.java new file mode 100644 index 0000000..af4c964 --- /dev/null +++ b/src/backend/case-services/java-http-ok/src/main/java/ru/edu/portal/OkHttpServer.java @@ -0,0 +1,56 @@ +package ru.edu.portal; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.util.logging.Logger; + + +public class OkHttpServer { + private final static Logger LOG = Logger.getLogger("NOFRAMEWORKS"); + + public static void main(String[] args) throws IOException { + + final int port = Integer.parseInt(System.getProperty("appPort", "8500")); + + + final HttpServer server = HttpServer.create(new InetSocketAddress(port), 50); + server.createContext("/").setHandler(new Root()); + server.createContext("/health").setHandler(new Health()); + + server.start(); + } + + private static class Root implements HttpHandler { + + public Root() {} + + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().add("Content-Type", "text/html"); + exchange.sendResponseHeaders(200, 0); + var responseBody = exchange.getResponseBody(); + responseBody.write("

OK

".getBytes()); + exchange.getResponseBody().close(); + LOG.info(String.format("[GET] / CODE: 200 - %s", exchange.getRequestHeaders().get("User-Agent"))); + } + } + + private static class Health implements HttpHandler { + + public Health() {} + + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().add("Content-Type", "text/html"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + LOG.info(String.format("[GET] /health CODE: 200 - %s", exchange.getRequestHeaders().get("User-Agent"))); + } + } +} diff --git a/src/backend/engines/docker/.gitignore b/src/backend/engines/docker/.gitignore new file mode 100755 index 0000000..29b636a --- /dev/null +++ b/src/backend/engines/docker/.gitignore @@ -0,0 +1,2 @@ +.idea +*.iml \ No newline at end of file diff --git a/src/backend/engines/docker/api.go b/src/backend/engines/docker/api.go new file mode 100644 index 0000000..21818cb --- /dev/null +++ b/src/backend/engines/docker/api.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "log" +) + +type ApiServer struct { + router *gin.Engine +} + +func NewApiServer() *ApiServer { + r := gin.Default() + router(r) + return &ApiServer{r} +} + +func (s *ApiServer) Run(addr string) { + err := s.router.Run(addr) + if err != nil { + log.Fatal(err) + } +} + +func router(router *gin.Engine) { + //TODO добавить проверки запуска, возвращать разные коды + router.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "UP", + }) + }) +} diff --git a/src/backend/engines/docker/build.go b/src/backend/engines/docker/build.go new file mode 100755 index 0000000..51281f6 --- /dev/null +++ b/src/backend/engines/docker/build.go @@ -0,0 +1,125 @@ +package main + +import ( + "archive/tar" + "bytes" + "github.com/docker/docker/api/types" + "io" + "os" + "path/filepath" +) + +type BuildRequest struct { + DockerfileName string + ImageName string + types.BuilderVersion + dockerfile string + contextPath string +} + +func (c *Client) Build(buildRequest BuildRequest) (*string, error) { + + dockerFileTarReader, err := loadContext(buildRequest) + + imageBuildResponse, err := c.cli.ImageBuild( + c.ctx, + dockerFileTarReader, + types.ImageBuildOptions{ + Tags: []string{buildRequest.ImageName}, + Context: dockerFileTarReader, + Dockerfile: buildRequest.DockerfileName, + Version: buildRequest.BuilderVersion, + Remove: true}) + + if err != nil { + return nil, err + } + defer imageBuildResponse.Body.Close() + + logBuffer := new(bytes.Buffer) + _, err = io.Copy(logBuffer, imageBuildResponse.Body) + if err != nil { + return nil, err + } + log := logBuffer.String() + + return &log, nil +} + +func loadContext(buildRequest BuildRequest) (*bytes.Reader, error) { + + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + defer tw.Close() + + err := addDockerfile(buildRequest, tw) + if err != nil { + return nil, err + } + + err = addContextFiles(buildRequest.contextPath, tw) + if err != nil { + return nil, err + } + + return bytes.NewReader(buf.Bytes()), nil +} + +func addDockerfile(buildRequest BuildRequest, tarWriter *tar.Writer) error { + readDockerFile := []byte(buildRequest.dockerfile) + + tarHeader := &tar.Header{ + Name: buildRequest.DockerfileName, + Size: int64(len(readDockerFile)), + } + + err := tarWriter.WriteHeader(tarHeader) + if err != nil { + return err + } + _, err = tarWriter.Write(readDockerFile) + if err != nil { + return err + } + return nil +} + +func addContextFiles(filePath string, tarWriter *tar.Writer) error { + err := filepath.Walk(filePath, + func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + err = addFile(path, tarWriter) + if err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + return nil +} + +func addFile(filename string, tarWriter *tar.Writer) error { + body, err := os.ReadFile(filename) + if err != nil { + return err + } + fileBase := filepath.Base(filename) + tarHeader := &tar.Header{ + Name: fileBase, + Size: int64(len(body)), + } + err = tarWriter.WriteHeader(tarHeader) + if err != nil { + return err + } + _, err = tarWriter.Write(body) + if err != nil { + return err + } + + return nil +} diff --git a/src/backend/engines/docker/build_test.go b/src/backend/engines/docker/build_test.go new file mode 100755 index 0000000..9ab164a --- /dev/null +++ b/src/backend/engines/docker/build_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/docker/docker/api/types" + "log" + "strings" + "testing" + "time" +) + +const ( + df = ` +FROM python:3.8-alpine +COPY ./requirements.txt /app/requirements.txt +WORKDIR /app +RUN pip install -r requirements.txt +COPY . /app +ENTRYPOINT [ "python" ] + +CMD ["main.py" ] +` +) + +func TestBuild(t *testing.T) { + client := NewClient() + + result, err := client.Build( + BuildRequest{ + DockerfileName: "Dockerfile", + ImageName: fmt.Sprintf("image-%v-%s:latest", 1, time.Now().Format("20060102030405")), + BuilderVersion: types.BuilderV1, + dockerfile: df, + contextPath: "./contexts/case1/", + }) + if err != nil { + t.Error(err) + } + for _, line := range strings.Split(*result, "\n") { + if strings.HasPrefix(line, "{") { + newLine := LogLine{} + err := json.Unmarshal([]byte(line), &newLine) + if err != nil { + log.Printf("ERR: %s\n", err.Error()) + log.Printf("Line: \n%s\n", line) + } + if newLine.Error != nil { + t.Errorf("BUILD ERR: %s\n", *newLine.Error) + } + } + } +} + +func TestName(t *testing.T) { + name := fmt.Sprintf("image-%v-%s:latest", 1, time.Now().Format("20060102030405")) + t.Log(name) + t.Fail() +} diff --git a/src/backend/engines/docker/client.go b/src/backend/engines/docker/client.go new file mode 100755 index 0000000..47dd483 --- /dev/null +++ b/src/backend/engines/docker/client.go @@ -0,0 +1,23 @@ +package main + +import ( + "context" + "github.com/docker/docker/client" +) + +type Client struct { + cli *client.Client + ctx context.Context +} + +func NewClient() *Client { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + panic(err) + } + client := Client{ + cli, + context.Background(), + } + return &client +} diff --git a/src/backend/engines/docker/containers.go b/src/backend/engines/docker/containers.go new file mode 100755 index 0000000..031a1df --- /dev/null +++ b/src/backend/engines/docker/containers.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "log" +) + +type StartRequest struct { + ImageName string +} + +func (c *Client) StartContainer(startRequest StartRequest) (*string, error) { + + containerResponse, err := c.cli.ContainerCreate( + c.ctx, + &container.Config{ + Image: startRequest.ImageName, + Tty: false}, + &container.HostConfig{}, + &network.NetworkingConfig{}, + nil, + "") + + if err != nil { + return nil, err + } + + log.Printf("Start container ID: %s\n", containerResponse.ID) + + err = c.cli.ContainerStart(c.ctx, containerResponse.ID, container.StartOptions{}) + if err != nil { + log.Printf("ERROR: %s\n", err.Error()) + } + return &containerResponse.ID, nil +} diff --git a/src/backend/engines/docker/containers_test.go b/src/backend/engines/docker/containers_test.go new file mode 100755 index 0000000..f3f8259 --- /dev/null +++ b/src/backend/engines/docker/containers_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "github.com/docker/docker/api/types/container" + "testing" + "time" +) + +func TestStartContainer(t *testing.T) { + client := NewClient() + + id, err := client.StartContainer(StartRequest{ImageName: "flask-app:latest"}) + if err != nil { + t.Error(err) + } + t.Log(id) + + //TODO wait container ??? + time.Sleep(3 * time.Second) + + caseTest := TestCase{ + CaseId: 1, + CaseType: "request", + Context: ContextRequest{ + 5000, + "/hello", + "", + "GET", + 200, + "Hi, Bro!", + }, + } + status, err := client.TestRun(*id, caseTest) + if !status { + t.Error(err) + } + + err = client.cli.ContainerStop(client.ctx, *id, container.StopOptions{}) + if err != nil { + t.Error(err) + } + err = client.cli.ContainerRemove(client.ctx, *id, container.RemoveOptions{}) + if err != nil { + t.Error(err) + } +} + +func TestContextContainer(t *testing.T) { + client := NewClient() + + time.Sleep(3 * time.Second) + + caseTest := TestCase{ + CaseId: 2, + CaseType: "context", + Context: ContextTest{ + "$.Size", + "<", + "60000000", + }, + } + status, err := client.TestRun("flask-app:latest", caseTest) + t.Log("status", status, err) + if !status { + t.Error(err) + } +} diff --git a/src/backend/engines/docker/contexts/base/case1/app.jar b/src/backend/engines/docker/contexts/base/case1/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..83b7e4cc9926cae18f80c2139cfa9256ea58327f GIT binary patch literal 5020 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 literal 0 HcmV?d00001 diff --git a/src/backend/engines/docker/contexts/base/case1/request.json b/src/backend/engines/docker/contexts/base/case1/request.json new file mode 100644 index 0000000..8a617e3 --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case1/request.json @@ -0,0 +1,19 @@ +{ + "job_id": 1, + "engine_type": "docker", + "request_data": { + "main": "FROM eclipse-temurin:17-jre-alpine\n\nADD app.jar /opt\n\nCMD exec java -jar /opt/app.jar", + "context_path": "./contexts/base/case1/" + }, + "test_cases": [ + { + "case_id": 1, + "case_type": "context", + "context": { + "Field": "$.Size", + "Operator": "<", + "Value": "209715200" + } + } + ] +} \ No newline at end of file diff --git a/src/backend/engines/docker/contexts/base/case2/app.jar b/src/backend/engines/docker/contexts/base/case2/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..83b7e4cc9926cae18f80c2139cfa9256ea58327f GIT binary patch literal 5020 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 literal 0 HcmV?d00001 diff --git a/src/backend/engines/docker/contexts/base/case2/request.json b/src/backend/engines/docker/contexts/base/case2/request.json new file mode 100644 index 0000000..bac4ba9 --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case2/request.json @@ -0,0 +1,19 @@ +{ + "job_id": 1, + "engine_type": "docker", + "request_data": { + "main": "FROM openjdk:17\n\nLABEL org.opencontainers.image.authors=\"Anton Dzyk\"\n\nADD app.jar /opt\n\nCMD exec java -jar /opt/app.jar", + "context_path": "./contexts/base/case2/" + }, + "test_cases": [ + { + "case_id": 1, + "case_type": "context", + "context": { + "Field": "$.Config.Labels", + "Operator": "=", + "Value": "org.opencontainers.image.authors=Anton Dzyk" + } + } + ] +} \ No newline at end of file diff --git a/src/backend/engines/docker/contexts/base/case3/main.py b/src/backend/engines/docker/contexts/base/case3/main.py new file mode 100755 index 0000000..331a98c --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case3/main.py @@ -0,0 +1,10 @@ +from flask import Flask + +app = Flask(__name__) + +@app.route('/hello') +def hello(): + return 'Hello!' + +if __name__ == '__main__': + app.run(host="0.0.0.0") \ No newline at end of file diff --git a/src/backend/engines/docker/contexts/base/case3/request.json b/src/backend/engines/docker/contexts/base/case3/request.json new file mode 100644 index 0000000..2ede1e3 --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case3/request.json @@ -0,0 +1,22 @@ +{ + "job_id": 1, + "engine_type": "docker_buildx", + "request_data": { + "main": "FROM python:3.8-alpine\nADD https://gitflic.ru/project/dzyk/demo-flask.git /app\nWORKDIR /app\nRUN pip install -r requirements.txt\nCOPY . /app\nENTRYPOINT [\"python\"]\nCMD [\"main.py\"]", + "context_path": "./contexts/base/case2/" + }, + "test_cases": [ + { + "case_id": 1, + "case_type": "request", + "context": { + "container_port": 5000, + "url_path": "/hello", + "request_body": "", + "type_request": "GET", + "expected_code": 200, + "expected_body": "Hello!" + } + } + ] +} \ No newline at end of file diff --git a/src/backend/engines/docker/contexts/base/case3/requirements.txt b/src/backend/engines/docker/contexts/base/case3/requirements.txt new file mode 100755 index 0000000..c204bef --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case3/requirements.txt @@ -0,0 +1 @@ +Flask==3.0.0 \ No newline at end of file diff --git a/src/backend/engines/docker/contexts/base/case4/Dockerfile b/src/backend/engines/docker/contexts/base/case4/Dockerfile new file mode 100644 index 0000000..fbddf30 --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case4/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:21 + +ADD app.jar /opt + +WORKDIR /opt + +CMD ["/usr/bin/java", "-jar", "app.jar"] \ No newline at end of file diff --git a/src/backend/engines/docker/contexts/base/case4/app.jar b/src/backend/engines/docker/contexts/base/case4/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..83b7e4cc9926cae18f80c2139cfa9256ea58327f GIT binary patch literal 5020 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 literal 0 HcmV?d00001 diff --git a/src/backend/engines/docker/contexts/base/case4/request.json b/src/backend/engines/docker/contexts/base/case4/request.json new file mode 100644 index 0000000..def1ad4 --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case4/request.json @@ -0,0 +1,22 @@ +{ + "job_id": 1, + "engine_type": "docker", + "request_data": { + "main": "FROM openjdk:21\n\nADD app.jar /opt\n\nWORKDIR /opt\n\nCMD [\"/usr/bin/java\", \"-jar\", \"app.jar\"]", + "context_path": "./contexts/base/case4/" + }, + "test_cases": [ + { + "case_id": 1, + "case_type": "request", + "context": { + "container_port": 8500, + "url_path": "/health", + "request_body": "", + "type_request": "GET", + "expected_code": 200, + "expected_body": "" + } + } + ] +} \ No newline at end of file diff --git a/src/backend/engines/docker/contexts/base/case5/Dockerfile b/src/backend/engines/docker/contexts/base/case5/Dockerfile new file mode 100644 index 0000000..664115b --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case5/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.22.5 AS build +WORKDIR /go/src/ +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o case5 case5.go + +FROM scratch + +COPY --from=build /go/src/case5 . + +ENTRYPOINT ["/case5"] diff --git a/src/backend/engines/docker/contexts/base/case5/case5.go b/src/backend/engines/docker/contexts/base/case5/case5.go new file mode 100644 index 0000000..6095c7b --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case5/case5.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(w, "pong") + if err != nil { + log.Printf("ERR: %v", err) + } + }) + + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/src/backend/engines/docker/contexts/base/case5/request.json b/src/backend/engines/docker/contexts/base/case5/request.json new file mode 100644 index 0000000..e9d19a1 --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case5/request.json @@ -0,0 +1,22 @@ +{ + "job_id": 1, + "engine_type": "docker", + "request_data": { + "main": "FROM golang:1.22.5 AS build\nWORKDIR /go/src/\nCOPY . .\nRUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags=\"-w -s\" -o case5 case5.go\n\nFROM scratch\n\nCOPY --from=build /go/src/case5 .\n\nENTRYPOINT [\"/case5\"]", + "context_path": "./contexts/base/case5/" + }, + "test_cases": [ + { + "case_id": 1, + "case_type": "request", + "context": { + "container_port": 8080, + "url_path": "/ping", + "request_body": "", + "type_request": "GET", + "expected_code": 200, + "expected_body": "pong" + } + } + ] +} \ No newline at end of file diff --git a/src/backend/engines/docker/contexts/base/case_1_back.json b/src/backend/engines/docker/contexts/base/case_1_back.json new file mode 100644 index 0000000..61c1b38 --- /dev/null +++ b/src/backend/engines/docker/contexts/base/case_1_back.json @@ -0,0 +1,26 @@ +{ + "task_id": 1, + "job_id": 1, + "engine_type": "docker", + "description": "В отделе был разработан сервис на Java. Теперь нужно опубликовать его на тестовом стенде, на котором используется Kubernetes. Чтобы его опубликовать - необходимо запаковать его в Docker-контейнер. Вам неоходимо выбрать базовый образ для будущего образа. Но есть условие, чтобы размер образа не был больше 100Mb", + "examples": { + "main": "FROM ...\n\nADD app.jar /opt\n\nCMD exec java -jar /opt/app.jar" + }, + "tags": [ + "docker", + "FROM" + ], + "author": "Anton Dzyk", + "task_template": null, + "test_cases": [ + { + "case_id": 1, + "case_type": "context", + "context": { + "Field": "$.Size", + "Operator": "<", + "Value": "107374182400" + } + } + ] +} \ No newline at end of file diff --git a/src/backend/engines/docker/go.mod b/src/backend/engines/docker/go.mod new file mode 100755 index 0000000..a414ee2 --- /dev/null +++ b/src/backend/engines/docker/go.mod @@ -0,0 +1,72 @@ +module docker-engine + +go 1.21.0 + +toolchain go1.22.5 + +require ( + github.com/PaesslerAG/jsonpath v0.1.1 + github.com/docker/docker v27.2.1+incompatible + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/moby/buildkit v0.16.0 + github.com/twmb/franz-go v1.17.1 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/PaesslerAG/gval v1.0.0 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/twmb/franz-go/pkg/kmsg v1.8.0 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/grpc v1.62.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect +) diff --git a/src/backend/engines/docker/go.sum b/src/backend/engines/docker/go.sum new file mode 100755 index 0000000..7264aa2 --- /dev/null +++ b/src/backend/engines/docker/go.sum @@ -0,0 +1,206 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= +github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= +github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= +github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= +github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/buildkit v0.16.0 h1:wOVBj1o5YNVad/txPQNXUXdelm7Hs/i0PUFjzbK0VKE= +github.com/moby/buildkit v0.16.0/go.mod h1:Xqx/5GlrqE1yIRORk0NSCVDFpQAU1WjlT6KHYZdisIQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/twmb/franz-go v1.17.1 h1:0LwPsbbJeJ9R91DPUHSEd4su82WJWcTY1Zzbgbg4CeQ= +github.com/twmb/franz-go v1.17.1/go.mod h1:NreRdJ2F7dziDY/m6VyspWd6sNxHKXdMZI42UfQ3GXM= +github.com/twmb/franz-go/pkg/kmsg v1.8.0 h1:lAQB9Z3aMrIP9qF9288XcFf/ccaSxEitNA1CDTEIeTA= +github.com/twmb/franz-go/pkg/kmsg v1.8.0/go.mod h1:HzYEb8G3uu5XevZbtU0dVbkphaKTHk0X68N5ka4q6mU= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU= +google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= +google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/src/backend/engines/docker/image.go b/src/backend/engines/docker/image.go new file mode 100644 index 0000000..698bce6 --- /dev/null +++ b/src/backend/engines/docker/image.go @@ -0,0 +1,11 @@ +package main + +import "github.com/docker/docker/api/types" + +func (c *Client) ImageContent(name string) (*types.ImageInspect, error) { + inspect, _, err := c.cli.ImageInspectWithRaw(c.ctx, name) + if err != nil { + return nil, err + } + return &inspect, nil +} diff --git a/src/backend/engines/docker/kafka_client.go b/src/backend/engines/docker/kafka_client.go new file mode 100644 index 0000000..23b9a3a --- /dev/null +++ b/src/backend/engines/docker/kafka_client.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/google/uuid" + "github.com/twmb/franz-go/pkg/kgo" + "log" +) + +type KafkaClient struct { + client *kgo.Client + ctx context.Context +} + +func NewKafkaClient() *KafkaClient { + + seeds := []string{"127.0.0.1:29092"} + + cl, err := kgo.NewClient( + kgo.SeedBrokers(seeds...), + kgo.ConsumerGroup("edu-group"), + kgo.ConsumeTopics("edu_task_docker"), + ) + if err != nil { + panic(err) + } + + return &KafkaClient{ + cl, + context.Background(), + } + +} + +func (kc *KafkaClient) Consume() { + + for { + fetches := kc.client.PollFetches(kc.ctx) + if errs := fetches.Errors(); len(errs) > 0 { + panic(fmt.Sprint(errs)) + } + + fetches.EachPartition(func(p kgo.FetchTopicPartition) { + p.EachRecord(func(record *kgo.Record) { + task := &Task{} + err := json.Unmarshal(record.Value, &task) + if err != nil { + log.Printf("ERROR: %v", err.Error()) + } + TaskRun(task) + }) + }) + } +} + +func (kc *KafkaClient) Produce(message []byte) { + id := uuid.New().String() + record := &kgo.Record{Topic: "edu_results", Key: []byte(id), Value: message} + + if err := kc.client.ProduceSync(kc.ctx, record).FirstErr(); err != nil { + log.Printf("record had a produce error while synchronously producing: %v\n", err) + } +} + +func (kc *KafkaClient) Close() { + kc.client.Close() +} diff --git a/src/backend/engines/docker/main.go b/src/backend/engines/docker/main.go new file mode 100755 index 0000000..1f6020d --- /dev/null +++ b/src/backend/engines/docker/main.go @@ -0,0 +1,21 @@ +package main + +import "log" + +var ( + apiServer *ApiServer + KafkaCurrentClient *KafkaClient +) + +func main() { + log.Println("Start...") + apiServer = NewApiServer() + KafkaCurrentClient = NewKafkaClient() + + go func() { + defer KafkaCurrentClient.Close() + KafkaCurrentClient.Consume() + }() + + apiServer.Run(":8083") +} diff --git a/src/backend/engines/docker/network.go b/src/backend/engines/docker/network.go new file mode 100755 index 0000000..3493db0 --- /dev/null +++ b/src/backend/engines/docker/network.go @@ -0,0 +1,22 @@ +package main + +import ( + "github.com/docker/docker/api/types" + "log" +) + +func (c *Client) CreateNetwork() { + networkResponse, err := c.cli.NetworkCreate(c.ctx, "test-network", types.NetworkCreate{Driver: "bridge"}) + if err != nil { + log.Printf("error: %s", err.Error()) + } + log.Printf("Create network ID: %s\n", networkResponse.ID) +} + +func (c *Client) ListNetwork() { + networkList, _ := c.cli.NetworkList(c.ctx, types.NetworkListOptions{}) + log.Println("Network List:") + for number, network := range networkList { + log.Printf("# %d, Name: %s", number, network.Name) + } +} diff --git a/src/backend/engines/docker/prune.go b/src/backend/engines/docker/prune.go new file mode 100755 index 0000000..ec4d52e --- /dev/null +++ b/src/backend/engines/docker/prune.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "log" +) + +func (c *Client) Prune() { + log.Println("Start Prune") + + log.Println("Cache:") + report, _ := c.cli.BuildCachePrune(c.ctx, types.BuildCachePruneOptions{All: true}) + for _, nameCache := range report.CachesDeleted { + log.Printf("Name: %s\n", nameCache) + } + + log.Println("Containers:") + pruneReport, _ := c.cli.ContainersPrune(c.ctx, filters.Args{}) + for _, pruneContainer := range pruneReport.ContainersDeleted { + log.Printf("Name: %s\n", pruneContainer) + } + + log.Println("Volumes:") + pruneVolumes, _ := c.cli.VolumesPrune(c.ctx, filters.Args{}) + for _, pruneVolume := range pruneVolumes.VolumesDeleted { + log.Printf("Name: %s\n", pruneVolume) + } + + log.Println("Images:") + pruneImages, _ := c.cli.ImagesPrune(c.ctx, filters.Args{}) + for _, pruneImage := range pruneImages.ImagesDeleted { + log.Printf("Name: %s\n", pruneImage) + } + + log.Printf("Prune finish") +} diff --git a/src/backend/engines/docker/show_env.go b/src/backend/engines/docker/show_env.go new file mode 100755 index 0000000..fb0a546 --- /dev/null +++ b/src/backend/engines/docker/show_env.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "github.com/docker/docker/client" + "log" +) + +func (c *Client) PrintEnv() { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + panic(err) + } + + log.Printf("API Version: %s\n", cli.ClientVersion()) + ctx := context.Background() + p, err := cli.Ping(ctx) + if err != nil { + log.Printf("Ping Docker Engine error: %s\n", err.Error()) + } + log.Printf("API Version: %s, OSType: %s, Experimental: %t, BuilderVersion: %s\n", + p.APIVersion, p.OSType, p.Experimental, p.BuilderVersion) + + info, _ := cli.Info(ctx) + log.Printf("INFO\n CPUSet: %t, kernel version: %s\n", info.CPUSet, info.KernelVersion) + + log.Printf("Host address: %s\n", cli.DaemonHost()) + + /* + log.Printf("\n# Volumes #\n") + vList, _ := cli.VolumeList(ctx, volume.ListOptions{}) + for number, nameVolume := range vList.Volumes { + log.Printf("# %d - Name: %s", number, nameVolume.Name) + } + */ +} diff --git a/src/backend/engines/docker/struct.go b/src/backend/engines/docker/struct.go new file mode 100644 index 0000000..aa9d081 --- /dev/null +++ b/src/backend/engines/docker/struct.go @@ -0,0 +1,75 @@ +package main + +import ( + "encoding/json" +) + +type Task struct { + JobId int64 `json:"job_id"` + EngineType string `json:"engine_type"` + RequestData map[string]string `json:"request_data"` + TestCases []TestCase `json:"test_cases"` +} + +type TaskResult struct { + Status bool `json:"status"` + Log []string `json:"log"` + Result string `json:"result"` + JobId int64 `json:"job_id"` +} + +type TestCase struct { + CaseId int64 `json:"case_id"` + CaseType string `json:"case_type"` + Context interface{} `json:"context"` +} + +func (c *TestCase) getContextRequest() ContextRequest { + requestBytes, _ := json.Marshal(c.Context) + contextRequest := ContextRequest{} + json.Unmarshal(requestBytes, &contextRequest) + return contextRequest +} + +func (c *TestCase) getContextContext() ContextTest { + caseBytes, _ := json.Marshal(c.Context) + caseTest := ContextTest{} + json.Unmarshal(caseBytes, &caseTest) + return caseTest +} + +type ContextRequest struct { + ContainerPort int `json:"container_port"` + UrlPath string `json:"url_path"` + RequestBody string `json:"request_body"` + TypeRequest string `json:"type_request"` + ExpectedCode int `json:"expected_code"` + ExpectedBody string `json:"expected_body"` +} + +type ContextTest struct { + Field string `json:"field"` + Operator string `json:"operator"` + Value string `json:"value"` +} + +type LogLine struct { + Stream string `json:"stream,omitempty"` + Status string `json:"status,omitempty"` + *Aux `json:"aux,omitempty"` + Error *string `json:"error,omitempty"` + ErrorDetail *ErrorMessage `json:"errorMessage,omitempty"` +} + +type Aux struct { + ID string +} + +type ErrorMessage struct { + Message string `json:"message,omitempty"` +} + +type LogLine2 struct { + Id string `json:"id"` + Aux interface{} `json:"aux"` +} diff --git a/src/backend/engines/docker/struct_test.go b/src/backend/engines/docker/struct_test.go new file mode 100644 index 0000000..ab29986 --- /dev/null +++ b/src/backend/engines/docker/struct_test.go @@ -0,0 +1,22 @@ +package main + +import "testing" + +func TestTestCaseContextCast(t *testing.T) { + + var c interface{} = map[string]interface{}{ + "field": "$.xxx", + "operator": "<", + "value": "Test", + } + + test := TestCase{ + 1, + "docker", + c} + + res := test.getContextContext() + if res.Value != "Test" { + t.Fail() + } +} diff --git a/src/backend/engines/docker/task_runner.go b/src/backend/engines/docker/task_runner.go new file mode 100644 index 0000000..af25740 --- /dev/null +++ b/src/backend/engines/docker/task_runner.go @@ -0,0 +1,139 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "github.com/docker/docker/api/types" + controlapi "github.com/moby/buildkit/api/services/control" + "log" + "strings" + "time" +) + +func TaskRun(task *Task) { + client := NewClient() + + imageId := fmt.Sprintf("%v-%s:latest", task.JobId, time.Now().Format("20060102030405")) + result, err := client.Build(BuildRequest{ + DockerfileName: "Dockerfile", + ImageName: imageId, + BuilderVersion: getBuildVersion(task.EngineType), + dockerfile: task.RequestData["main"], + contextPath: task.RequestData["context_path"], + }) + if err != nil { + KafkaCurrentClient.Produce(TaskResultBody(task.JobId, false, err.Error(), nil)) + return + } + + errorMessages := make([]string, 0) + logMessage := make([]string, 0) + lastError := "" + + for _, line := range strings.Split(*result, "\n") { + if strings.HasPrefix(line, "{") { + ln, err := parseLog(line, getBuildVersion(task.EngineType)) + if err != nil { + errorMessages = append(errorMessages, fmt.Sprintf("ERR: %s\n", err.Error())) + lastError = err.Error() + } + if ln != nil { + logMessage = append(logMessage, fmt.Sprintf("%s\n", *ln)) + } + } + } + + if len(errorMessages) > 0 { + KafkaCurrentClient.Produce(TaskResultBody(task.JobId, false, lastError, errorMessages)) + return + } + + logMessage = append(logMessage, "\n\nRun Tests:\n") + finalStatus := true + for _, testCase := range task.TestCases { + status, err := client.TestRun(imageId, testCase) + if !status { + finalStatus = false + } + logMessage = append(logMessage, fmt.Sprintf("CaseId: %v, Result: %v, Err: %v", testCase.CaseId, status, err)) + } + KafkaCurrentClient.Produce(TaskResultBody(task.JobId, finalStatus, "", logMessage)) +} + +func TaskResultBody(jobId int64, status bool, errorMsg string, logMessages []string) []byte { + res := TaskResult{ + status, + logMessages, + errorMsg, + jobId, + } + + body, err := json.Marshal(res) + if err != nil { + log.Printf("ERR: %s\n", err.Error()) + } + return body +} + +func getBuildVersion(buildVersion string) types.BuilderVersion { + if buildVersion == "docker" { + return types.BuilderV1 + } + if buildVersion == "docker_buildx" { + return types.BuilderBuildKit + } + return types.BuilderV1 +} + +func parseLog(line string, version types.BuilderVersion) (*string, error) { + if version == types.BuilderV1 { + newLine := LogLine{} + err := json.Unmarshal([]byte(line), &newLine) + if err != nil { + return nil, err + } else { + if newLine.Error != nil { + return nil, fmt.Errorf(*newLine.Error) + } + return &newLine.Stream, nil + } + } + if version == types.BuilderBuildKit { + // TODO использоваться парсер из кода moby + newLine := LogLine2{} + err := json.Unmarshal([]byte(line), &newLine) + if err != nil { + return nil, err + } + + if newLine.Id == "moby.buildkit.trace" { + data, err := base64.StdEncoding.DecodeString(newLine.Aux.(string)) + aux := controlapi.StatusResponse{} + err = aux.Unmarshal(data) + if err != nil { + return nil, err + } else { + listDigest := make([]string, 0) + message := "" + for _, ln := range aux.Vertexes { + if ln.Completed != nil { + repeat := false + for _, dg := range listDigest { + if ln.Digest.Hex() == dg { + repeat = true + } + } + if repeat { + continue + } + listDigest = append(listDigest, ln.Digest.Hex()) + message = message + "\n" + ln.Name + } + } + return &message, nil + } + } + } + return nil, nil +} diff --git a/src/backend/engines/docker/test_engine.go b/src/backend/engines/docker/test_engine.go new file mode 100755 index 0000000..aeb2679 --- /dev/null +++ b/src/backend/engines/docker/test_engine.go @@ -0,0 +1,145 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/docker/docker/api/types/container" + "io" + "log" + "net/http" + "strconv" + "strings" + "time" + + jsonpath "github.com/PaesslerAG/jsonpath" +) + +func (c *Client) TestRun(ID string, testcase TestCase) (bool, error) { + switch testcase.CaseType { + case "request": + return c.curlCase(ID, testcase) + case "context": + return c.contextCase(ID, testcase) + default: + return false, fmt.Errorf("not support case type") + } +} + +func (c *Client) curlCase(ID string, testcase TestCase) (bool, error) { + + id, err := c.StartContainer(StartRequest{ImageName: ID}) + if err != nil { + return false, err + } + defer c.stopContainer(id) + + //TODO wait container ??? + time.Sleep(3 * time.Second) + + info, err := c.cli.ContainerInspect(c.ctx, *id) + if err != nil { + return false, err + } + log.Printf("Get info container %s, IP:%s\n", info.Name, info.NetworkSettings.IPAddress) + var IPAddress = info.NetworkSettings.IPAddress + + contextCase := testcase.getContextRequest() + + body := strings.NewReader(contextCase.RequestBody) + request, err := http.NewRequest(contextCase.TypeRequest, fmt.Sprintf("http://%s:%d%s", + IPAddress, contextCase.ContainerPort, contextCase.UrlPath), body) + if err != nil { + return false, err + } + resp, err := http.DefaultClient.Do(request) + if err != nil { + return false, err + } + + if resp.StatusCode != contextCase.ExpectedCode { + return false, fmt.Errorf("Response CODE does not match\nResult: %d\nExpected: %d", + resp.StatusCode, contextCase.ExpectedCode) + } + + if contextCase.ExpectedBody != "" { + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + strBody := string(body) + if strBody != contextCase.ExpectedBody { + return false, fmt.Errorf("Response body does not match\nResult: %s\nExpected: %s", + strBody, contextCase.ExpectedBody) + } + } + + return true, nil +} + +func (c *Client) stopContainer(ID *string) { + _ = c.cli.ContainerStop(c.ctx, *ID, container.StopOptions{}) + _ = c.cli.ContainerRemove(c.ctx, *ID, container.RemoveOptions{}) +} + +func (c *Client) contextCase(ID string, testcase TestCase) (bool, error) { + inspect, err := c.ImageContent(ID) + if err != nil { + return false, err + } + contextTest := testcase.getContextContext() + + body, err := json.Marshal(inspect) + if err != nil { + return false, err + } + bodyObject := interface{}(nil) + err = json.Unmarshal(body, &bodyObject) + if err != nil { + return false, err + } + result, err := jsonpath.Get(contextTest.Field, bodyObject) + if err != nil { + return false, err + } + + switch contextTest.Operator { + case "<": + res := lessInt(result.(float64), contextTest.Value) + var err error + if !res { + err = fmt.Errorf("does not satisfy the condition %v < %s", result, contextTest.Value) + } + return res, err + case "=": + resultStr := resultByString(result) + res := contextTest.Value == resultStr + var err error + if !res { + err = fmt.Errorf("expected: %s, result: %s", contextTest.Value, resultStr) + } + return res, err + } + return false, nil +} + +func lessInt(intA float64, intB string) bool { + intBVal, _ := strconv.ParseFloat(intB, 64) + if intA < intBVal { + return true + } + return false +} + +func resultByString(result interface{}) string { + if res, ok := result.(string); ok { + return res + } + if res, ok := result.(map[string]interface{}); ok { + newStr := "" + for k, v := range res { + newStr = newStr + k + "=" + fmt.Sprintf("%v", v) + } + return newStr + } + return "" +} diff --git a/src/frontend/user-frontend/.eslintrc.cjs b/src/frontend/user-frontend/.eslintrc.cjs new file mode 100644 index 0000000..ade8571 --- /dev/null +++ b/src/frontend/user-frontend/.eslintrc.cjs @@ -0,0 +1,14 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + root: true, + 'extends': [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript' + ], + parserOptions: { + ecmaVersion: 'latest' + } +} diff --git a/src/frontend/user-frontend/Dockerfile b/src/frontend/user-frontend/Dockerfile new file mode 100644 index 0000000..7205a56 --- /dev/null +++ b/src/frontend/user-frontend/Dockerfile @@ -0,0 +1,14 @@ +FROM node:16.20.2 AS build + +COPY . /build +WORKDIR /build + +RUN npm install +RUN npm job build + +FROM nginx:1.26.2 + +COPY --from=build /build/dist /app +COPY nginx.conf /default.conf + +CMD ["/bin/sh", "-c", "DOLLAR=$ envsubst < /default.conf > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"] diff --git a/src/frontend/user-frontend/README.md b/src/frontend/user-frontend/README.md new file mode 100644 index 0000000..ecbe7d3 --- /dev/null +++ b/src/frontend/user-frontend/README.md @@ -0,0 +1,39 @@ +# user-frontend + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm job dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm job build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +npm job lint +``` diff --git a/src/frontend/user-frontend/components.d.ts b/src/frontend/user-frontend/components.d.ts new file mode 100644 index 0000000..48c269e --- /dev/null +++ b/src/frontend/user-frontend/components.d.ts @@ -0,0 +1,25 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + HomePage: typeof import('./src/components/HomePage.vue')['default'] + IconCommunity: typeof import('./src/components/icons/IconCommunity.vue')['default'] + IconDocumentation: typeof import('./src/components/icons/IconDocumentation.vue')['default'] + IconEcosystem: typeof import('./src/components/icons/IconEcosystem.vue')['default'] + IconSupport: typeof import('./src/components/icons/IconSupport.vue')['default'] + IconTooling: typeof import('./src/components/icons/IconTooling.vue')['default'] + Profile: typeof import('./src/components/Profile.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + Runner: typeof import('./src/components/Runner.vue')['default'] + Tasks: typeof import('./src/components/Tasks.vue')['default'] + Themes: typeof import('./src/components/Themes.vue')['default'] + TheWelcome: typeof import('@/components/HomePage.vue')['default'] + WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default'] + } +} diff --git a/src/frontend/user-frontend/env.d.ts b/src/frontend/user-frontend/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/frontend/user-frontend/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/frontend/user-frontend/index.html b/src/frontend/user-frontend/index.html new file mode 100644 index 0000000..0292b82 --- /dev/null +++ b/src/frontend/user-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Education Portal + + +

+ + + diff --git a/src/frontend/user-frontend/jsconfig.json b/src/frontend/user-frontend/jsconfig.json new file mode 100644 index 0000000..4f25534 --- /dev/null +++ b/src/frontend/user-frontend/jsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "allowJs": true, + "target": "es5", + "module": "esnext", + "baseUrl": "./", + "moduleResolution": "node", + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + } +} diff --git a/src/frontend/user-frontend/nginx.conf b/src/frontend/user-frontend/nginx.conf new file mode 100644 index 0000000..772170b --- /dev/null +++ b/src/frontend/user-frontend/nginx.conf @@ -0,0 +1,27 @@ +server { + listen 80; + server_name localhost; + client_max_body_size 10M; + + gzip on; + gzip_comp_level 5; + gzip_static on; + + location /api { + proxy_pass ${API_BACKEND}; + proxy_set_header X-Forwarded-Proto ${DOLLAR}scheme; + proxy_set_header X-Forwarded-Port ${DOLLAR}server_port; + proxy_set_header X-Forwarded-For ${DOLLAR}proxy_add_x_forwarded_for; + proxy_set_header Host ${DOLLAR}http_host; + } + + location / { + root /usr/share/nginx/html/; + index index.html index.htm; + try_files ${DOLLAR}uri /index.html; + } + + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/src/frontend/user-frontend/package-lock.json b/src/frontend/user-frontend/package-lock.json new file mode 100644 index 0000000..080c98d --- /dev/null +++ b/src/frontend/user-frontend/package-lock.json @@ -0,0 +1,3651 @@ +{ + "name": "user-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "user-frontend", + "version": "0.0.0", + "dependencies": { + "axios": "^1.7.7", + "chartist": "^1.3.0", + "core-js": "^3.38.1", + "keycloak-js": "^24.0.5", + "vue": "^3.4.29", + "vue-axios": "^3.5.2", + "vue-router": "^4.3.3" + }, + "devDependencies": { + "@rushstack/eslint-patch": "^1.8.0", + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.14.5", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/tsconfig": "^0.5.1", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.23.0", + "npm-run-all2": "^6.2.0", + "typescript": "~5.4.0", + "unplugin-fonts": "^1.1.1", + "unplugin-vue-components": "^0.27.4", + "unplugin-vue-router": "^0.10.8", + "vite": "^5.3.1", + "vite-plugin-vue-layouts": "^0.11.0", + "vite-plugin-vuetify": "^2.0.4", + "vue-tsc": "^2.0.21" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz", + "integrity": "sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz", + "integrity": "sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz", + "integrity": "sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz", + "integrity": "sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz", + "integrity": "sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz", + "integrity": "sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz", + "integrity": "sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz", + "integrity": "sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz", + "integrity": "sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz", + "integrity": "sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz", + "integrity": "sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz", + "integrity": "sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz", + "integrity": "sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz", + "integrity": "sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz", + "integrity": "sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz", + "integrity": "sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "dev": true + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.4.tgz", + "integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.3.tgz", + "integrity": "sha512-3xbWsKEKXYlmX82aOHufFQVnkbMC/v8fLpWwh6hWOUrK5fbbtBh9Q/WWse27BFgSy2/e2c0fz5Scgya9h2GLhw==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.4.tgz", + "integrity": "sha512-kO9k4kTLfxpg+6lq7/KAIv3m2d62IHuCL6GbVgYZTpfKvIGoAIlDxK7pFcB/eczN2+ydg/vnyaeZ6SGyZrJw2w==", + "dev": true, + "dependencies": { + "@volar/source-map": "2.4.4" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.4.tgz", + "integrity": "sha512-xG3PZqOP2haG8XG4Pg3PD1UGDAdqZg24Ru8c/qYjYAnmcj6GBR64mstx+bZux5QOyRaJK+/lNM/RnpvBD3489g==", + "dev": true + }, + "node_modules/@volar/typescript": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.4.tgz", + "integrity": "sha512-QQMQRVj0fVHJ3XdRKiS1LclhG0VBXdFYlyuHRQF/xLk2PuJuHNWP26MDZNvEVCvnyUQuUQhIAfylwY5TGPgc6w==", + "dev": true, + "dependencies": { + "@volar/language-core": "2.4.4", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-macros/common": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-1.12.3.tgz", + "integrity": "sha512-dlSqrGdIDhqMOz92XtlMNyuHHeHe594O6f10XLtmlB0Jrq/Pl4Hj8rXAnVlRdjg+ptbZRSNL6MSgOPPoC82owg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.6", + "@rollup/pluginutils": "^5.1.0", + "@vue/compiler-sfc": "^3.5.3", + "ast-kit": "^1.1.0", + "local-pkg": "^0.5.0", + "magic-string-ast": "^0.6.2" + }, + "engines": { + "node": ">=16.14.0" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.3.tgz", + "integrity": "sha512-adAfy9boPkP233NTyvLbGEqVuIfK/R0ZsBsIOW4BZNfb4BRpRW41Do1u+ozJpsb+mdoy80O20IzAsHaihRb5qA==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.3", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.3.tgz", + "integrity": "sha512-wnzFArg9zpvk/811CDOZOadJRugf1Bgl/TQ3RfV4nKfSPok4hi0w10ziYUQR6LnnBAUlEXYLUfZ71Oj9ds/+QA==", + "dependencies": { + "@vue/compiler-core": "3.5.3", + "@vue/shared": "3.5.3" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.3.tgz", + "integrity": "sha512-P3uATLny2tfyvMB04OQFe7Sczteno7SLFxwrOA/dw01pBWQHB5HL15a8PosoNX2aG/EAMGqnXTu+1LnmzFhpTQ==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.3", + "@vue/compiler-dom": "3.5.3", + "@vue/compiler-ssr": "3.5.3", + "@vue/shared": "3.5.3", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.44", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.3.tgz", + "integrity": "sha512-F/5f+r2WzL/2YAPl7UlKcJWHrvoZN8XwEBLnT7S4BXwncH25iDOabhO2M2DWioyTguJAGavDOawejkFXj8EM1w==", + "dependencies": { + "@vue/compiler-dom": "3.5.3", + "@vue/shared": "3.5.3" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==" + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz", + "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "peerDependencies": { + "eslint": "^8.56.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": ">=4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==", + "dev": true, + "dependencies": { + "@volar/language-core": "~2.4.1", + "@vue/compiler-dom": "^3.4.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.3.tgz", + "integrity": "sha512-2w61UnRWTP7+rj1H/j6FH706gRBHdFVpIqEkSDAyIpafBXYH8xt4gttstbbCWdU3OlcSWO8/3mbKl/93/HSMpw==", + "dependencies": { + "@vue/shared": "3.5.3" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.3.tgz", + "integrity": "sha512-5b2AQw5OZlmCzSsSBWYoZOsy75N4UdMWenTfDdI5bAzXnuVR7iR8Q4AOzQm2OGoA41xjk53VQKrqQhOz2ktWaw==", + "dependencies": { + "@vue/reactivity": "3.5.3", + "@vue/shared": "3.5.3" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.3.tgz", + "integrity": "sha512-wPR1DEGc3XnQ7yHbmkTt3GoY0cEnVGQnARRdAkDzZ8MbUKEs26gogCQo6AOvvgahfjIcnvWJzkZArQ1fmWjcSg==", + "dependencies": { + "@vue/reactivity": "3.5.3", + "@vue/runtime-core": "3.5.3", + "@vue/shared": "3.5.3", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.3.tgz", + "integrity": "sha512-28volmaZVG2PGO3V3+gBPKoSHvLlE8FGfG/GKXKkjjfxLuj/50B/0OQGakM/g6ehQeqCrZYM4eHC4Ks48eig1Q==", + "dependencies": { + "@vue/compiler-ssr": "3.5.3", + "@vue/shared": "3.5.3" + }, + "peerDependencies": { + "vue": "3.5.3" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.3.tgz", + "integrity": "sha512-Jp2v8nylKBT+PlOUjun2Wp/f++TfJVFjshLzNtJDdmFJabJa7noGMncqXRM1vXGX+Yo2V7WykQFNxusSim8SCA==" + }, + "node_modules/@vue/tsconfig": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.5.1.tgz", + "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", + "dev": true + }, + "node_modules/@vuetify/loader-shared": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz", + "integrity": "sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg==", + "dev": true, + "dependencies": { + "upath": "^2.0.1" + }, + "peerDependencies": { + "vue": "^3.0.0", + "vuetify": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ast-kit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.1.0.tgz", + "integrity": "sha512-RlNqd4u6c/rJ5R+tN/ZTtyNrH8X0NHCvyt6gD8RHa3JjzxxHWoyaU0Ujk3Zjbh7IZqrYl1Sxm6XzZifmVxXxHQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "pathe": "^1.1.2" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz", + "integrity": "sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "ast-kit": "^1.0.1" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chartist": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/chartist/-/chartist-1.3.0.tgz", + "integrity": "sha512-M3ckI3ua7EHt08WLZvdi3QXn5g+in27qU6TxjI5bpriS6QwIZlWtisyUhFbpGclW546SlT3SL9oq0vFFDiAo6g==", + "engines": { + "node": ">=14" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true + }, + "node_modules/core-js": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", + "integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz", + "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-sha256": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", + "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/keycloak-js": { + "version": "24.0.5", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-24.0.5.tgz", + "integrity": "sha512-VQOSn3j13DPB6OuavKAq+sRjDERhIKrXgBzekoHRstifPuyULILguugX6yxRUYFSpn3OMYUXmSX++tkdCupOjA==", + "dependencies": { + "js-sha256": "^0.11.0", + "jwt-decode": "^4.0.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magic-string-ast": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-0.6.2.tgz", + "integrity": "sha512-oN3Bcd7ZVt+0VGEs7402qR/tjgjbM7kPlH/z7ufJnzTLVBzXJITRHOJiwMmmYMgZfdoWQsfQcY+iKlxiBppnMA==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.10" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-all2": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-6.2.2.tgz", + "integrity": "sha512-Q+alQAGIW7ZhKcxLt8GcSi3h3ryheD6xnmXahkMRVM5LYmajcUrSITm8h+OPC9RYWMV2GR0Q1ntTUCfxaNoOJw==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.3", + "memorystream": "^0.3.1", + "minimatch": "^9.0.0", + "pidtree": "^0.6.0", + "read-package-json-fast": "^3.0.2", + "shell-quote": "^1.7.3" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^14.18.0 || ^16.13.0 || >=18.0.0", + "npm": ">= 8" + } + }, + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pkg-types": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", + "integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==", + "dev": true, + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.7.1", + "pathe": "^1.1.2" + } + }, + "node_modules/postcss": { + "version": "8.4.45", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", + "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.2.tgz", + "integrity": "sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.2", + "@rollup/rollup-android-arm64": "4.21.2", + "@rollup/rollup-darwin-arm64": "4.21.2", + "@rollup/rollup-darwin-x64": "4.21.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.2", + "@rollup/rollup-linux-arm-musleabihf": "4.21.2", + "@rollup/rollup-linux-arm64-gnu": "4.21.2", + "@rollup/rollup-linux-arm64-musl": "4.21.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.2", + "@rollup/rollup-linux-riscv64-gnu": "4.21.2", + "@rollup/rollup-linux-s390x-gnu": "4.21.2", + "@rollup/rollup-linux-x64-gnu": "4.21.2", + "@rollup/rollup-linux-x64-musl": "4.21.2", + "@rollup/rollup-win32-arm64-msvc": "4.21.2", + "@rollup/rollup-win32-ia32-msvc": "4.21.2", + "@rollup/rollup-win32-x64-msvc": "4.21.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unplugin": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.0.tgz", + "integrity": "sha512-cfkZeALGyW7tKYjZbi0G+pn0XnUFa0QvLIeLJEUUlnU0R8YYsBQnt5+h9Eu1B7AB7KETld+UBFI5lOeBL+msoQ==", + "dev": true, + "dependencies": { + "acorn": "^8.12.1", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "webpack-sources": "^3" + }, + "peerDependenciesMeta": { + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/unplugin-fonts": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unplugin-fonts/-/unplugin-fonts-1.1.1.tgz", + "integrity": "sha512-/Aw/rL9D2aslGGM0vi+2R2aG508RSwawLnnBuo+JDSqYc4cHJO1R1phllhN6GysEhBp/6a4B6+vSFPVapWyAAw==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.12", + "unplugin": "^1.3.1" + }, + "peerDependencies": { + "@nuxt/kit": "^3.0.0", + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.4.tgz", + "integrity": "sha512-1XVl5iXG7P1UrOMnaj2ogYa5YTq8aoh5jwDPQhemwO/OrXW+lPQKDXd1hMz15qxQPxgb/XXlbgo3HQ2rLEbmXQ==", + "dev": true, + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "chokidar": "^3.6.0", + "debug": "^4.3.6", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "minimatch": "^9.0.5", + "mlly": "^1.7.1", + "unplugin": "^1.12.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-router": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.10.8.tgz", + "integrity": "sha512-xi+eLweYAqolIoTRSmumbi6Yx0z5M0PLvl+NFNVWHJgmE2ByJG1SZbrn+TqyuDtIyln20KKgq8tqmL7aLoiFjw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.4", + "@rollup/pluginutils": "^5.1.0", + "@vue-macros/common": "^1.12.2", + "ast-walker-scope": "^0.6.2", + "chokidar": "^3.6.0", + "fast-glob": "^3.3.2", + "json5": "^2.2.3", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "scule": "^1.3.0", + "unplugin": "^1.12.2", + "yaml": "^2.5.0" + }, + "peerDependencies": { + "vue-router": "^4.4.0" + }, + "peerDependenciesMeta": { + "vue-router": { + "optional": true + } + } + }, + "node_modules/upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz", + "integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-layouts": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-layouts/-/vite-plugin-vue-layouts-0.11.0.tgz", + "integrity": "sha512-uh6NW7lt+aOXujK4eHfiNbeo55K9OTuB7fnv+5RVc4OBn/cZull6ThXdYH03JzKanUfgt6QZ37NbbtJ0og59qw==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "fast-glob": "^3.3.2" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.4", + "vue-router": "^4.0.11" + } + }, + "node_modules/vite-plugin-vuetify": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.4.tgz", + "integrity": "sha512-A4cliYUoP/u4AWSRVRvAPKgpgR987Pss7LpFa7s1GvOe8WjgDq92Rt3eVXrvgxGCWvZsPKziVqfHHdCMqeDhfw==", + "dev": true, + "dependencies": { + "@vuetify/loader-shared": "^2.0.3", + "debug": "^4.3.3", + "upath": "^2.0.1" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": ">=5", + "vue": "^3.0.0", + "vuetify": "^3.0.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, + "node_modules/vue": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.3.tgz", + "integrity": "sha512-xvRbd0HpuLovYbOHXRHlSBsSvmUJbo0pzbkKTApWnQGf3/cu5Z39mQeA5cZdLRVIoNf3zI6MSoOgHUT5i2jO+Q==", + "dependencies": { + "@vue/compiler-dom": "3.5.3", + "@vue/compiler-sfc": "3.5.3", + "@vue/runtime-dom": "3.5.3", + "@vue/server-renderer": "3.5.3", + "@vue/shared": "3.5.3" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-axios": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/vue-axios/-/vue-axios-3.5.2.tgz", + "integrity": "sha512-GP+dct7UlAWkl1qoP3ppw0z6jcSua5/IrMpjB5O8bh089iIiJ+hdxPYH2NPEpajlYgkW5EVMP95ttXWdas1O0g==", + "peerDependencies": { + "axios": "*", + "vue": "^3.0.0 || ^2.0.0" + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.3.tgz", + "integrity": "sha512-sv6wmNKx2j3aqJQDMxLFzs/u/mjA9Z5LCgy6BE0f7yFWMjrPLnS/sPNn8ARY/FXw6byV18EFutn5lTO6+UsV5A==", + "dependencies": { + "@vue/devtools-api": "^6.6.3" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.6.tgz", + "integrity": "sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==", + "dev": true, + "dependencies": { + "@volar/typescript": "~2.4.1", + "@vue/language-core": "2.1.6", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vuetify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.1.tgz", + "integrity": "sha512-N1XlczbgeGt/O+JUk72QPrqcDaRIXUdptUciJqGyTvZ9cfMoSlEWs6TZO+dOOfXbKvmIMFMycYg4dgSHDpCPhg==", + "dev": true, + "peer": true, + "engines": { + "node": "^12.20 || >=14.13" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "typescript": ">=4.7", + "vite-plugin-vuetify": ">=1.0.0", + "vue": "^3.3.0", + "webpack-plugin-vuetify": ">=2.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite-plugin-vuetify": { + "optional": true + }, + "webpack-plugin-vuetify": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/frontend/user-frontend/package.json b/src/frontend/user-frontend/package.json new file mode 100644 index 0000000..0aa26ab --- /dev/null +++ b/src/frontend/user-frontend/package.json @@ -0,0 +1,41 @@ +{ + "name": "user-frontend", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "type-check": "vue-tsc --build --force", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + }, + "dependencies": { + "axios": "^1.7.7", + "core-js": "^3.38.1", + "keycloak-js": "^24.0.5", + "vue": "^3.4.29", + "vue-axios": "^3.5.2", + "vue-router": "^4.3.3", + "chartist": "^1.3.0" + }, + "devDependencies": { + "@rushstack/eslint-patch": "^1.8.0", + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.14.5", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/tsconfig": "^0.5.1", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.23.0", + "npm-run-all2": "^6.2.0", + "typescript": "~5.4.0", + "unplugin-fonts": "^1.1.1", + "unplugin-vue-components": "^0.27.4", + "unplugin-vue-router": "^0.10.8", + "vite": "^5.3.1", + "vite-plugin-vue-layouts": "^0.11.0", + "vite-plugin-vuetify": "^2.0.4", + "vue-tsc": "^2.0.21" + } +} diff --git a/src/frontend/user-frontend/src/App.vue b/src/frontend/user-frontend/src/App.vue new file mode 100644 index 0000000..5383139 --- /dev/null +++ b/src/frontend/user-frontend/src/App.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/frontend/user-frontend/src/assets/base.css b/src/frontend/user-frontend/src/assets/base.css new file mode 100644 index 0000000..8816868 --- /dev/null +++ b/src/frontend/user-frontend/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/src/frontend/user-frontend/src/assets/logo.ico b/src/frontend/user-frontend/src/assets/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..80e3cac1d314fc41ba6cbb8f4ec702fe7d14c24e GIT binary patch literal 1150 zcmZQzU<5(|0R|wcz>vYhz#zuJz@P!dKp~(AL>x#lH~{5F0cr>YtoQj3!h!3(?|?9f z9kMw@8YT{-LAw0c`@8`0kxf9Bhl!($!SoQT-+!I=mY|KE*2w08*re$9-|TIRt{>!% z!1X?FVD1ar5u^cQ1I@kx!jbzUMPOp+`a$-C + + + + + + diff --git a/src/frontend/user-frontend/src/assets/main.css b/src/frontend/user-frontend/src/assets/main.css new file mode 100644 index 0000000..6759b0f --- /dev/null +++ b/src/frontend/user-frontend/src/assets/main.css @@ -0,0 +1,30 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/src/frontend/user-frontend/src/assets/project_goals.png b/src/frontend/user-frontend/src/assets/project_goals.png new file mode 100644 index 0000000000000000000000000000000000000000..8db49301019579dc8881f3847d802909c7d2f446 GIT binary patch literal 42254 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�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 literal 0 HcmV?d00001 diff --git a/src/frontend/user-frontend/src/components/HomePage.vue b/src/frontend/user-frontend/src/components/HomePage.vue new file mode 100644 index 0000000..a1ae2eb --- /dev/null +++ b/src/frontend/user-frontend/src/components/HomePage.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/frontend/user-frontend/src/components/Profile.vue b/src/frontend/user-frontend/src/components/Profile.vue new file mode 100644 index 0000000..45c98b1 --- /dev/null +++ b/src/frontend/user-frontend/src/components/Profile.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/src/frontend/user-frontend/src/components/Runner.vue b/src/frontend/user-frontend/src/components/Runner.vue new file mode 100644 index 0000000..26a6ce8 --- /dev/null +++ b/src/frontend/user-frontend/src/components/Runner.vue @@ -0,0 +1,156 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/user-frontend/src/components/Tasks.vue b/src/frontend/user-frontend/src/components/Tasks.vue new file mode 100644 index 0000000..1fcb6aa --- /dev/null +++ b/src/frontend/user-frontend/src/components/Tasks.vue @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/src/frontend/user-frontend/src/components/Themes.vue b/src/frontend/user-frontend/src/components/Themes.vue new file mode 100644 index 0000000..40c2bde --- /dev/null +++ b/src/frontend/user-frontend/src/components/Themes.vue @@ -0,0 +1,61 @@ + + + + + + \ No newline at end of file diff --git a/src/frontend/user-frontend/src/components/WelcomeItem.vue b/src/frontend/user-frontend/src/components/WelcomeItem.vue new file mode 100644 index 0000000..6d7086a --- /dev/null +++ b/src/frontend/user-frontend/src/components/WelcomeItem.vue @@ -0,0 +1,87 @@ + + + diff --git a/src/frontend/user-frontend/src/components/icons/IconCommunity.vue b/src/frontend/user-frontend/src/components/icons/IconCommunity.vue new file mode 100644 index 0000000..2dc8b05 --- /dev/null +++ b/src/frontend/user-frontend/src/components/icons/IconCommunity.vue @@ -0,0 +1,7 @@ + diff --git a/src/frontend/user-frontend/src/components/icons/IconDocumentation.vue b/src/frontend/user-frontend/src/components/icons/IconDocumentation.vue new file mode 100644 index 0000000..6d4791c --- /dev/null +++ b/src/frontend/user-frontend/src/components/icons/IconDocumentation.vue @@ -0,0 +1,7 @@ + diff --git a/src/frontend/user-frontend/src/components/icons/IconEcosystem.vue b/src/frontend/user-frontend/src/components/icons/IconEcosystem.vue new file mode 100644 index 0000000..c3a4f07 --- /dev/null +++ b/src/frontend/user-frontend/src/components/icons/IconEcosystem.vue @@ -0,0 +1,7 @@ + diff --git a/src/frontend/user-frontend/src/components/icons/IconSupport.vue b/src/frontend/user-frontend/src/components/icons/IconSupport.vue new file mode 100644 index 0000000..7452834 --- /dev/null +++ b/src/frontend/user-frontend/src/components/icons/IconSupport.vue @@ -0,0 +1,7 @@ + diff --git a/src/frontend/user-frontend/src/components/icons/IconTooling.vue b/src/frontend/user-frontend/src/components/icons/IconTooling.vue new file mode 100644 index 0000000..660598d --- /dev/null +++ b/src/frontend/user-frontend/src/components/icons/IconTooling.vue @@ -0,0 +1,19 @@ + + diff --git a/src/frontend/user-frontend/src/main.ts b/src/frontend/user-frontend/src/main.ts new file mode 100644 index 0000000..1896651 --- /dev/null +++ b/src/frontend/user-frontend/src/main.ts @@ -0,0 +1,23 @@ +// Plugins +import { registerPlugins } from '@/plugins' + +import './assets/main.css' + +import App from './App.vue' +import { createApp } from 'vue' + +import { login } from '@/plugins/keycloak' +import { setupInterceptors } from '@/plugins/axios' +import {setupRouteWatch} from '@/plugins/router' + + +login(() => { + setupInterceptors() + setupRouteWatch() + + const app = createApp(App) + + registerPlugins(app) + + app.mount('#app') +}) diff --git a/src/frontend/user-frontend/src/plugins/axios.js b/src/frontend/user-frontend/src/plugins/axios.js new file mode 100644 index 0000000..46a8fda --- /dev/null +++ b/src/frontend/user-frontend/src/plugins/axios.js @@ -0,0 +1,22 @@ +import axios from 'axios' +import router from '../router' +import { updateToken } from '@/plugins/keycloak' + +const AUTHORIZATION_HEADER = 'Authorization' + +export function setupInterceptors() { + axios.interceptors.request.use(async config => { + const token = await updateToken() + config.headers[AUTHORIZATION_HEADER] = `Bearer ${token}` + return config + }) + + axios.interceptors.response.use( (response) => { + return response + }, error => { + return new Promise((resolve, reject) => { + router.push('/error') + reject(error) + }) + }) +} diff --git a/src/frontend/user-frontend/src/plugins/index.js b/src/frontend/user-frontend/src/plugins/index.js new file mode 100644 index 0000000..ec90a24 --- /dev/null +++ b/src/frontend/user-frontend/src/plugins/index.js @@ -0,0 +1,9 @@ +import router from '@/router' +import axios from 'axios' +import VueAxios from 'vue-axios' + +export function registerPlugins(app) { + app + .use(router) + .use(VueAxios, axios) +} diff --git a/src/frontend/user-frontend/src/plugins/keycloak.js b/src/frontend/user-frontend/src/plugins/keycloak.js new file mode 100644 index 0000000..a82ae4c --- /dev/null +++ b/src/frontend/user-frontend/src/plugins/keycloak.js @@ -0,0 +1,40 @@ +import Keycloak from 'keycloak-js' + +const initOptions = { + url: import.meta.env.VITE_KEYCLOAK_URL, + realm: 'edu-portal', + clientId: 'api' +} + +const TOKEN_MIN_VALIDITY_SECONDS = 70 + +export const keycloak = new Keycloak(initOptions) + +export async function updateToken() { + await keycloak.updateToken(TOKEN_MIN_VALIDITY_SECONDS) + return keycloak.token +} + +export function login(onAuthenticatedCallback) { + keycloak.init({ onLoad: 'login-required' }).then((auth) => { + if (auth) { + onAuthenticatedCallback() + + window.onfocus = () => { + updateToken() + } + } else { + window.location.reload() + } + }) +} + +export function getRole() { + var currentRole = '' + keycloak.realmAccess['roles'].map(function(role) { + if (['USER', 'CURATOR', 'ADMIN'].includes(role) === true) { + currentRole = role + } + }) + return currentRole +} diff --git a/src/frontend/user-frontend/src/plugins/router.js b/src/frontend/user-frontend/src/plugins/router.js new file mode 100644 index 0000000..f38bdcc --- /dev/null +++ b/src/frontend/user-frontend/src/plugins/router.js @@ -0,0 +1,9 @@ +import { useRoute } from 'vue-router' +import { watch } from 'vue' +import { updateToken } from '@/plugins/keycloak' + +const route = useRoute() + +export function setupRouteWatch() { + watch(route, () => updateToken()) +} diff --git a/src/frontend/user-frontend/src/router/index.ts b/src/frontend/user-frontend/src/router/index.ts new file mode 100644 index 0000000..b7c4011 --- /dev/null +++ b/src/frontend/user-frontend/src/router/index.ts @@ -0,0 +1,45 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '../views/HomeView.vue' +import Themes from '@/components/Themes.vue' +import Tasks from '@/components/Tasks.vue' +import Runner from '@/components/Runner.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + component: HomeView + }, + { + path: '/themes', + name: 'themes', + component: Themes + }, + { + path: '/theme/:id', + name: 'theme', + component: Tasks, + props: true + }, + { + path: '/task/:id', + name: 'task', + component: Runner, + props: true + }, + { + path: '/about', + name: 'about', + component: () => import('../views/AboutView.vue') + }, + { + path: '/error', + name: 'error', + component: () => import('../views/ErrorView.vue') + } + ] +}) + +export default router diff --git a/src/frontend/user-frontend/src/views/AboutView.vue b/src/frontend/user-frontend/src/views/AboutView.vue new file mode 100644 index 0000000..4999abc --- /dev/null +++ b/src/frontend/user-frontend/src/views/AboutView.vue @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/src/frontend/user-frontend/src/views/ErrorView.vue b/src/frontend/user-frontend/src/views/ErrorView.vue new file mode 100644 index 0000000..f44f1d1 --- /dev/null +++ b/src/frontend/user-frontend/src/views/ErrorView.vue @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/src/frontend/user-frontend/src/views/HomeView.vue b/src/frontend/user-frontend/src/views/HomeView.vue new file mode 100644 index 0000000..c2efc57 --- /dev/null +++ b/src/frontend/user-frontend/src/views/HomeView.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/frontend/user-frontend/tsconfig.app.json b/src/frontend/user-frontend/tsconfig.app.json new file mode 100644 index 0000000..e14c754 --- /dev/null +++ b/src/frontend/user-frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/src/frontend/user-frontend/tsconfig.json b/src/frontend/user-frontend/tsconfig.json new file mode 100644 index 0000000..66b5e57 --- /dev/null +++ b/src/frontend/user-frontend/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/src/frontend/user-frontend/tsconfig.node.json b/src/frontend/user-frontend/tsconfig.node.json new file mode 100644 index 0000000..f094063 --- /dev/null +++ b/src/frontend/user-frontend/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/src/frontend/user-frontend/typed-router.d.ts b/src/frontend/user-frontend/typed-router.d.ts new file mode 100644 index 0000000..507aa91 --- /dev/null +++ b/src/frontend/user-frontend/typed-router.d.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️ +// It's recommended to commit this file. +// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. + +declare module 'vue-router/auto-routes' { + import type { + RouteRecordInfo, + ParamValue, + ParamValueOneOrMore, + ParamValueZeroOrMore, + ParamValueZeroOrOne, + } from 'vue-router' + + /** + * Route name map generated by unplugin-vue-router + */ + export interface RouteNamedMap { + } +} diff --git a/src/frontend/user-frontend/vite.config.mjs b/src/frontend/user-frontend/vite.config.mjs new file mode 100644 index 0000000..f8546a3 --- /dev/null +++ b/src/frontend/user-frontend/vite.config.mjs @@ -0,0 +1,54 @@ +// Plugins +import Components from 'unplugin-vue-components/vite' +import Vue from '@vitejs/plugin-vue' +import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' +import ViteFonts from 'unplugin-fonts/vite' +import Layouts from 'vite-plugin-vue-layouts' +import VueRouter from 'unplugin-vue-router/vite' + +// Utilities +import { defineConfig } from 'vite' +import { fileURLToPath, URL } from 'node:url' +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + VueRouter(), + Layouts(), + Vue({ + template: { transformAssetUrls } + }), + Components() + ], + define: { 'process.env': {} }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + extensions: [ + '.js', + '.json', + '.jsx', + '.mjs', + '.ts', + '.tsx', + '.vue', + ], + }, + server: { + host: 'portal.local', + port: 8081, + cors: false, + proxy: { + '^/api/': { + target: 'http://portal.local:8082', + changeOrigin: true, + secure: false, + }, + '^/keycloak/': { + target: 'http://portal.local:8080', + changeOrigin: true, + secure: false, + } + } + }, +}) diff --git a/src/frontend/user-frontend/vite.config.ts b/src/frontend/user-frontend/vite.config.ts new file mode 100644 index 0000000..5c45e1d --- /dev/null +++ b/src/frontend/user-frontend/vite.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +})