From 967be9e750221ab2ab783f95df79bb26d290a45e Mon Sep 17 00:00:00 2001 From: Martial Simon Date: Mon, 15 Sep 2025 01:07:58 +0200 Subject: add: added projects --- ping/.env | 1 + ping/.gitignore | 174 + ping/.gitlab-ci.yml | 92 + ping/README.md | 93 + ping/docker-compose.yml | 58 + ping/frontend/.dockerignore | 2 + ping/frontend/.gitignore | 23 + ping/frontend/.npmrc | 1 + ping/frontend/.prettierignore | 6 + ping/frontend/.prettierrc | 15 + ping/frontend/Dockerfile | 8 + ping/frontend/README.md | 38 + ping/frontend/eslint.config.js | 36 + ping/frontend/package-lock.json | 4385 ++++++++++++++++++++ ping/frontend/package.json | 44 + ping/frontend/src/app.css | 58 + ping/frontend/src/app.d.ts | 13 + ping/frontend/src/app.html | 13 + ping/frontend/src/lib/components/Avatar.svelte | 48 + ping/frontend/src/lib/components/Button.svelte | 17 + ping/frontend/src/lib/components/NavBar.svelte | 44 + .../src/lib/components/NumberStatList.svelte | 55 + ping/frontend/src/lib/components/SideBar.svelte | 61 + .../src/lib/components/SteppedLineChart.svelte | 76 + ping/frontend/src/lib/components/ToastList.svelte | 67 + ping/frontend/src/lib/components/UserItem.svelte | 42 + .../lib/components/dashboard/RiskAnalysis.svelte | 10 + .../src/lib/components/dashboard/StockGraph.svelte | 136 + .../components/dashboard/TrendingSymbols.svelte | 85 + .../dashboard/transactions/TransactionModal.svelte | 165 + .../src/lib/components/input/StockSelector.svelte | 231 ++ .../src/lib/components/input/UserSelector.svelte | 35 + ping/frontend/src/lib/pages.ts | 54 + ping/frontend/src/lib/stores/auth.ts | 66 + ping/frontend/src/lib/stores/toast.ts | 22 + ping/frontend/src/routes/+layout.svelte | 9 + ping/frontend/src/routes/+page.svelte | 59 + ping/frontend/src/routes/dashboard/+layout.svelte | 44 + ping/frontend/src/routes/dashboard/+page.svelte | 81 + .../src/routes/dashboard/analyses/+page.svelte | 409 ++ .../src/routes/dashboard/messages/+page.svelte | 403 ++ .../src/routes/dashboard/models/+page.svelte | 568 +++ .../src/routes/dashboard/personnel/+page.svelte | 290 ++ .../src/routes/dashboard/settings/+page.svelte | 452 ++ .../src/routes/dashboard/transactions/+page.svelte | 360 ++ ping/frontend/src/routes/login/+page.svelte | 149 + .../frontend/src/routes/stocksapi/chart/+server.ts | 21 + .../src/routes/stocksapi/insights/+server.ts | 19 + .../frontend/src/routes/stocksapi/quote/+server.ts | 18 + .../src/routes/stocksapi/search/+server.ts | 18 + .../routes/stocksapi/trendingSymbols/+server.ts | 16 + ping/frontend/static/favicon.png | Bin 0 -> 2329 bytes ping/frontend/static/icons/ICI | 2 + ping/frontend/static/icons/add-green.svg | 7 + ping/frontend/static/icons/bell.svg | 25 + ping/frontend/static/icons/closed-wallet.svg | 17 + ping/frontend/static/icons/coin.svg | 11 + ping/frontend/static/icons/credit-card.svg | 4 + ping/frontend/static/icons/dashboard.svg | 5 + ping/frontend/static/icons/debug.svg | 7 + ping/frontend/static/icons/edit-pen.svg | 5 + ping/frontend/static/icons/floppy-disk.svg | 18 + ping/frontend/static/icons/leaf.svg | 19 + .../static/icons/magnifying_glass_icon.svg | 5 + ping/frontend/static/icons/money-bills.svg | 27 + ping/frontend/static/icons/msg.svg | 7 + ping/frontend/static/icons/people.svg | 7 + ping/frontend/static/icons/settings.svg | 30 + ping/frontend/static/icons/side-menu.svg | 6 + ping/frontend/static/icons/trash.svg | 35 + ping/frontend/static/icons/triangle-down.svg | 12 + ping/frontend/static/icons/triangle-up.svg | 11 + ping/frontend/static/icons/wallet.svg | 15 + ping/frontend/static/img/default-avatar.png | Bin 0 -> 6483 bytes ping/frontend/static/img/favicon.svg | 29 + ping/frontend/static/img/header-bg.jpg | Bin 0 -> 53678 bytes ping/frontend/static/img/logo.svg | 41 + ping/frontend/static/img/logoFull.svg | 75 + ping/frontend/svelte.config.js | 18 + ping/frontend/tsconfig.json | 19 + ping/frontend/vite.config.ts | 30 + ping/ping/.env | 3 + ping/ping/pom.xml | 307 ++ .../ping/api/requests/ExecFeatureRequest.java | 20 + .../assistants/ping/api/requests/LoginRequest.java | 16 + .../assistants/ping/api/requests/MoveRequest.java | 17 + .../ping/api/requests/NewProjectRequest.java | 15 + .../ping/api/requests/NewTransactionRequest.java | 21 + .../ping/api/requests/NewUserRequest.java | 17 + .../assistants/ping/api/requests/PathRequest.java | 15 + .../ping/api/requests/UpdateProjectRequest.java | 16 + .../api/requests/UpdateTransactionRequest.java | 8 + .../ping/api/requests/UpdateUserRequest.java | 17 + .../ping/api/requests/UserProjectRequest.java | 17 + .../ping/api/responses/EcoScoreResponse.java | 9 + .../ping/api/responses/FSEntryResponse.java | 17 + .../ping/api/responses/LoginResponse.java | 15 + .../ping/api/responses/ProjectResponse.java | 22 + .../ping/api/responses/TransactionResponse.java | 30 + .../ping/api/responses/TransactionsResponse.java | 11 + .../ping/api/responses/UserResponse.java | 23 + .../ping/api/responses/UserSummaryResponse.java | 21 + .../assistants/ping/converters/IConverter.java | 17 + .../TransactionCreationEntityConverter.java | 23 + .../request/TransactionUpdateEntityConverter.java | 23 + .../request/UserCreationEntityConverter.java | 19 + .../request/UserUpdateEntityConverter.java | 19 + .../response/ProjectResponseConverter.java | 25 + .../response/TransactionResponseConverter.java | 25 + .../converters/response/UserResponseConverter.java | 21 + .../response/UserSummaryResponseConverter.java | 20 + .../assistants/ping/data/model/ProjectModel.java | 43 + .../ping/data/model/TransactionModel.java | 61 + .../assistants/ping/data/model/UserModel.java | 42 + .../ping/data/repository/ProjectRepository.java | 113 + .../data/repository/TransactionRepository.java | 32 + .../ping/data/repository/UserRepository.java | 120 + .../domain/entity/TransactionCreationEntity.java | 19 + .../domain/entity/TransactionUpdateEntity.java | 19 + .../ping/domain/entity/UserCreationEntity.java | 7 + .../ping/domain/entity/UserUpdateEntity.java | 7 + .../ping/domain/entity/feature/Feature.java | 14 + .../domain/entity/feature/FeatureExecution.java | 18 + .../ping/domain/executor/FeatureDispatcher.java | 31 + .../ping/domain/executor/FeatureExecutor.java | 17 + .../ping/domain/executor/GitFeatureExecutor.java | 134 + .../ping/domain/service/FileSystemService.java | 497 +++ .../ping/domain/service/ProjectService.java | 451 ++ .../ping/domain/service/TransactionService.java | 119 + .../ping/domain/service/UserService.java | 280 ++ .../epita/assistants/ping/errors/ErrorsCode.java | 36 + .../ping/errors/InvalidCommandException.java | 11 + .../ping/errors/InvalidFeatureException.java | 9 + .../assistants/ping/errors/PingServiceError.java | 25 + .../ping/errors/UnreacheableCodeException.java | 7 + .../ping/presentation/rest/DebugResource.java | 64 + .../ping/presentation/rest/HelloWorldResource.java | 27 + .../presentation/rest/TransactionResource.java | 115 + .../ping/presentation/rest/UtilsResource.java | 19 + .../rest/fileSystemEndpoints/FileResource.java | 108 + .../rest/fileSystemEndpoints/FolderResource.java | 80 + .../rest/projectEndpoints/ProjectResource.java | 154 + .../rest/userEndpoints/UserResource.java | 149 + .../fr/epita/assistants/ping/utils/ErrorInfo.java | 14 + .../fr/epita/assistants/ping/utils/HttpError.java | 40 + .../fr/epita/assistants/ping/utils/IHttpError.java | 7 + .../fr/epita/assistants/ping/utils/Logger.java | 129 + .../epita/assistants/ping/utils/TokenFactory.java | 27 + .../ping/src/main/resources/application.properties | 21 + ping/ping/src/main/resources/openapi.yml | 1065 +++++ ping/ping/src/main/resources/privateKey.pem | 28 + ping/ping/src/main/resources/publicKey.pem | 9 + .../data/repositories/ProjectFakeRepository.java | 111 + .../ping/data/repositories/UserFakeRepository.java | 121 + .../presentation/rest/TestHelloWorldResource.java | 31 + .../presentation/rest/TestProjectResource.java | 32 + .../ping/presentation/rest/TestUserResource.java | 431 ++ .../ping/src/test/resources/application.properties | 0 158 files changed, 15400 insertions(+) create mode 100644 ping/.env create mode 100644 ping/.gitignore create mode 100644 ping/.gitlab-ci.yml create mode 100644 ping/README.md create mode 100644 ping/docker-compose.yml create mode 100644 ping/frontend/.dockerignore create mode 100644 ping/frontend/.gitignore create mode 100644 ping/frontend/.npmrc create mode 100644 ping/frontend/.prettierignore create mode 100644 ping/frontend/.prettierrc create mode 100644 ping/frontend/Dockerfile create mode 100644 ping/frontend/README.md create mode 100644 ping/frontend/eslint.config.js create mode 100644 ping/frontend/package-lock.json create mode 100644 ping/frontend/package.json create mode 100644 ping/frontend/src/app.css create mode 100644 ping/frontend/src/app.d.ts create mode 100644 ping/frontend/src/app.html create mode 100644 ping/frontend/src/lib/components/Avatar.svelte create mode 100644 ping/frontend/src/lib/components/Button.svelte create mode 100644 ping/frontend/src/lib/components/NavBar.svelte create mode 100644 ping/frontend/src/lib/components/NumberStatList.svelte create mode 100644 ping/frontend/src/lib/components/SideBar.svelte create mode 100644 ping/frontend/src/lib/components/SteppedLineChart.svelte create mode 100644 ping/frontend/src/lib/components/ToastList.svelte create mode 100644 ping/frontend/src/lib/components/UserItem.svelte create mode 100644 ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte create mode 100644 ping/frontend/src/lib/components/dashboard/StockGraph.svelte create mode 100644 ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte create mode 100644 ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte create mode 100644 ping/frontend/src/lib/components/input/StockSelector.svelte create mode 100644 ping/frontend/src/lib/components/input/UserSelector.svelte create mode 100644 ping/frontend/src/lib/pages.ts create mode 100644 ping/frontend/src/lib/stores/auth.ts create mode 100644 ping/frontend/src/lib/stores/toast.ts create mode 100644 ping/frontend/src/routes/+layout.svelte create mode 100644 ping/frontend/src/routes/+page.svelte create mode 100644 ping/frontend/src/routes/dashboard/+layout.svelte create mode 100644 ping/frontend/src/routes/dashboard/+page.svelte create mode 100644 ping/frontend/src/routes/dashboard/analyses/+page.svelte create mode 100644 ping/frontend/src/routes/dashboard/messages/+page.svelte create mode 100644 ping/frontend/src/routes/dashboard/models/+page.svelte create mode 100644 ping/frontend/src/routes/dashboard/personnel/+page.svelte create mode 100644 ping/frontend/src/routes/dashboard/settings/+page.svelte create mode 100644 ping/frontend/src/routes/dashboard/transactions/+page.svelte create mode 100644 ping/frontend/src/routes/login/+page.svelte create mode 100644 ping/frontend/src/routes/stocksapi/chart/+server.ts create mode 100644 ping/frontend/src/routes/stocksapi/insights/+server.ts create mode 100644 ping/frontend/src/routes/stocksapi/quote/+server.ts create mode 100644 ping/frontend/src/routes/stocksapi/search/+server.ts create mode 100644 ping/frontend/src/routes/stocksapi/trendingSymbols/+server.ts create mode 100644 ping/frontend/static/favicon.png create mode 100644 ping/frontend/static/icons/ICI create mode 100644 ping/frontend/static/icons/add-green.svg create mode 100644 ping/frontend/static/icons/bell.svg create mode 100644 ping/frontend/static/icons/closed-wallet.svg create mode 100644 ping/frontend/static/icons/coin.svg create mode 100644 ping/frontend/static/icons/credit-card.svg create mode 100644 ping/frontend/static/icons/dashboard.svg create mode 100644 ping/frontend/static/icons/debug.svg create mode 100644 ping/frontend/static/icons/edit-pen.svg create mode 100644 ping/frontend/static/icons/floppy-disk.svg create mode 100644 ping/frontend/static/icons/leaf.svg create mode 100644 ping/frontend/static/icons/magnifying_glass_icon.svg create mode 100644 ping/frontend/static/icons/money-bills.svg create mode 100644 ping/frontend/static/icons/msg.svg create mode 100644 ping/frontend/static/icons/people.svg create mode 100644 ping/frontend/static/icons/settings.svg create mode 100644 ping/frontend/static/icons/side-menu.svg create mode 100644 ping/frontend/static/icons/trash.svg create mode 100644 ping/frontend/static/icons/triangle-down.svg create mode 100644 ping/frontend/static/icons/triangle-up.svg create mode 100644 ping/frontend/static/icons/wallet.svg create mode 100644 ping/frontend/static/img/default-avatar.png create mode 100644 ping/frontend/static/img/favicon.svg create mode 100644 ping/frontend/static/img/header-bg.jpg create mode 100644 ping/frontend/static/img/logo.svg create mode 100644 ping/frontend/static/img/logoFull.svg create mode 100644 ping/frontend/svelte.config.js create mode 100644 ping/frontend/tsconfig.json create mode 100644 ping/frontend/vite.config.ts create mode 100644 ping/ping/.env create mode 100644 ping/ping/pom.xml create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/ExecFeatureRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/LoginRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/MoveRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewProjectRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewTransactionRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewUserRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/PathRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateProjectRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateTransactionRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateUserRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UserProjectRequest.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/EcoScoreResponse.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/FSEntryResponse.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/LoginResponse.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/ProjectResponse.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionResponse.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionsResponse.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserResponse.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserSummaryResponse.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/IConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionCreationEntityConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionUpdateEntityConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserCreationEntityConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserUpdateEntityConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/ProjectResponseConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/TransactionResponseConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserResponseConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserSummaryResponseConverter.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/data/model/ProjectModel.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/data/model/TransactionModel.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/data/model/UserModel.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/ProjectRepository.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/TransactionRepository.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/UserRepository.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionCreationEntity.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionUpdateEntity.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserCreationEntity.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserUpdateEntity.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/Feature.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/FeatureExecution.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureDispatcher.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureExecutor.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/GitFeatureExecutor.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/FileSystemService.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/ProjectService.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/TransactionService.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/UserService.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/errors/ErrorsCode.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidCommandException.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidFeatureException.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/errors/PingServiceError.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/errors/UnreacheableCodeException.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/DebugResource.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/HelloWorldResource.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/TransactionResource.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/UtilsResource.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FileResource.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FolderResource.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/projectEndpoints/ProjectResource.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/userEndpoints/UserResource.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/utils/ErrorInfo.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/utils/HttpError.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/utils/IHttpError.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/utils/Logger.java create mode 100644 ping/ping/src/main/java/fr/epita/assistants/ping/utils/TokenFactory.java create mode 100644 ping/ping/src/main/resources/application.properties create mode 100644 ping/ping/src/main/resources/openapi.yml create mode 100644 ping/ping/src/main/resources/privateKey.pem create mode 100644 ping/ping/src/main/resources/publicKey.pem create mode 100644 ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/ProjectFakeRepository.java create mode 100644 ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/UserFakeRepository.java create mode 100644 ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestHelloWorldResource.java create mode 100644 ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestProjectResource.java create mode 100644 ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestUserResource.java create mode 100644 ping/ping/src/test/resources/application.properties (limited to 'ping') diff --git a/ping/.env b/ping/.env new file mode 100644 index 0000000..dc9d11a --- /dev/null +++ b/ping/.env @@ -0,0 +1 @@ +PROJECT_DEFAULT_PATH = ping \ No newline at end of file diff --git a/ping/.gitignore b/ping/.gitignore new file mode 100644 index 0000000..512f0b1 --- /dev/null +++ b/ping/.gitignore @@ -0,0 +1,174 @@ +.idea/ +.vscode/ +node_modules +flake.lock +*.tar +target/ +*.iml +viewer/ + +# Created by https://www.toptal.com/developers/gitignore/api/java,intellij,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=java,intellij,visualstudiocode + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/java,intellij,visualstudiocode + +# Temporary files +*.swp* diff --git a/ping/.gitlab-ci.yml b/ping/.gitlab-ci.yml new file mode 100644 index 0000000..299a656 --- /dev/null +++ b/ping/.gitlab-ci.yml @@ -0,0 +1,92 @@ +stages: + - test + - deploy + + +.connect_to_intra_git: + image: + name: alpine/git:2.47.1 + entrypoint: [""] + environment: + name: forge-intra + url: https://intra.forge.epita.fr/epita-ing-assistants-yaka/ping-2027/root/ping-2027/ping-backend + before_script: + - test -n "$GIT_INTRA_REPO_URL" + - test -n "$GIT_INTRA_GLOBAL_URL" + - test -n "$GIT_DEPLOY_BRANCH" + - test -n "$GIT_RUNNER_NAME" + - test -n "$GIT_RUNNER_EMAIL" + + - 'command -v ssh-agent >/dev/null || apk add --no-cache openssh' + - eval $(ssh-agent -s) + + - chmod 400 "$SSH_PRIVATE_KEY" + - ssh-add "$SSH_PRIVATE_KEY" + + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + + - ssh-keyscan -v -H $GIT_INTRA_GLOBAL_URL >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + + - ssh -T git@"$GIT_INTRA_GLOBAL_URL" + + - git config --global user.email "$RUNNER_GIT_EMAIL" + - git config --global user.name "$RUNNER_GIT_NAME" + + - git ls-remote --exit-code -h "$GIT_INTRA_REPO_URL" || { [ $? -ne 2 ] && return 1; } + + - git remote | grep 'intra' || git remote add intra "$GIT_INTRA_REPO_URL" + - git remote update + + +compilation-job: + stage: test + image: maven:3.9.9-amazoncorretto-21-alpine + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: always + script: + - cd ping + - mvn quarkus:build + + +push-master-state-to-intra-job: + extends: .connect_to_intra_git + stage: deploy + rules: + - if: $CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "push" + when: always + script: + - git log --all --oneline --graph + - git push intra HEAD:"$GIT_DEPLOY_BRANCH" + + +push-to-intra-job: + extends: .connect_to_intra_git + stage: deploy + rules: + - if: $CI_COMMIT_TAG != null + && ($CI_COMMIT_TAG =~ "/^release.*$/" + || $CI_COMMIT_TAG =~ "/^ping-backend-dementor-.*$/" + || $CI_COMMIT_TAG =~ "/^ping-backend-submission-.*$/" + || $CI_COMMIT_TAG =~ "/^ping-backend-continuous-tag-.*$/") + when: always + script: + - git push intra "$CI_COMMIT_TAG" + + +release-job: + stage: deploy + image: registry.gitlab.com/gitlab-org/release-cli:latest + rules: + - if: $CI_COMMIT_TAG != null + when: always + needs: + - job: push-to-intra-job + artifacts: false + script: + - echo "creating a new release for tag $CI_COMMIT_TAG..." + release: + tag_name: '$CI_COMMIT_TAG' + description: '$CI_COMMIT_TAG' diff --git a/ping/README.md b/ping/README.md new file mode 100644 index 0000000..aa308d2 --- /dev/null +++ b/ping/README.md @@ -0,0 +1,93 @@ +# PING + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://gitlab.cri.epita.fr/martial.simon/ping.git +git branch -M main +git push -uf origin main +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://gitlab.cri.epita.fr/martial.simon/ping/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README + +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/ping/docker-compose.yml b/ping/docker-compose.yml new file mode 100644 index 0000000..b9d5289 --- /dev/null +++ b/ping/docker-compose.yml @@ -0,0 +1,58 @@ +services: + db-postgres: + container_name: db-postgres + environment: + - PGDATA=$HOME/postgres_data + - PGHOST=/tmp + - PGPORT=5432 + - POSTGRES_HOST_AUTH_METHOD=trust # pas de mdp + - POSTGRES_USER=postgres + - POSTGRES_DB=ping + image: reg.undercloud.cri.epita.fr/docker/postgres:17.5-alpine3.22 + ports: + - 0.0.0.0:5432:5432 # pas safe pour la prod + networks: + - backend + + java-backend: + container_name: java-backend + image: reg.undercloud.cri.epita.fr/docker/maven:3.9.9-amazoncorretto-21-alpine + volumes: + - ./ping/pom.xml:/app/pom.xml + - ./ping/src:/app/src + working_dir: /app + command: sh -c "apk add --no-cache curl git && mvn clean install && mvn quarkus:dev -Dquarkus.http.host=0.0.0.0 -Dquarkus.datasource.jdbc.url=jdbc:postgresql://db-postgres:5432/ping" + depends_on: + - db-postgres + ports: + - 0.0.0.0:8080:8080 # pas safe pour la prod + networks: + - backend + - frontend + environment: + - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://db-postgres:5432/ping + + sveltekit-frontend: + container_name: sveltekit-frontend + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - 3000:3000 + depends_on: + - java-backend + environment: + - VITE_BACKEND_URL=http://java-backend:8080 + - ALPHA_VANTAGE_API_KEY=102780243600020474604 + networks: + - frontend + volumes: + - ./frontend:/tmp/ping/frontend + - /tmp/ping/frontend/node_modules # pour éviter les conflits de dépendances entre hôte et conteneur + working_dir: /tmp/ping/frontend + +networks: + backend: + driver: bridge + frontend: + driver: bridge diff --git a/ping/frontend/.dockerignore b/ping/frontend/.dockerignore new file mode 100644 index 0000000..bcf8731 --- /dev/null +++ b/ping/frontend/.dockerignore @@ -0,0 +1,2 @@ +.svelte-kit +node_modules \ No newline at end of file diff --git a/ping/frontend/.gitignore b/ping/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/ping/frontend/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/ping/frontend/.npmrc b/ping/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/ping/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/ping/frontend/.prettierignore b/ping/frontend/.prettierignore new file mode 100644 index 0000000..6562bcb --- /dev/null +++ b/ping/frontend/.prettierignore @@ -0,0 +1,6 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb diff --git a/ping/frontend/.prettierrc b/ping/frontend/.prettierrc new file mode 100644 index 0000000..7ebb855 --- /dev/null +++ b/ping/frontend/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/ping/frontend/Dockerfile b/ping/frontend/Dockerfile new file mode 100644 index 0000000..52b38c4 --- /dev/null +++ b/ping/frontend/Dockerfile @@ -0,0 +1,8 @@ +FROM node:24-alpine +WORKDIR /tmp/ping/frontend + +COPY package*.json ./ +RUN npm install + +COPY . . +ENTRYPOINT npm run dev -- --host 0.0.0.0 --port 3000 \ No newline at end of file diff --git a/ping/frontend/README.md b/ping/frontend/README.md new file mode 100644 index 0000000..b5b2950 --- /dev/null +++ b/ping/frontend/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/ping/frontend/eslint.config.js b/ping/frontend/eslint.config.js new file mode 100644 index 0000000..be96133 --- /dev/null +++ b/ping/frontend/eslint.config.js @@ -0,0 +1,36 @@ +import prettier from 'eslint-config-prettier'; +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { 'no-undef': 'off' } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/ping/frontend/package-lock.json b/ping/frontend/package-lock.json new file mode 100644 index 0000000..95da3b3 --- /dev/null +++ b/ping/frontend/package-lock.json @@ -0,0 +1,4385 @@ +{ + "name": "frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.1", + "dependencies": { + "chart.js": "^4.5.0", + "yahoo-finance2": "^2.13.3" + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "^4.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.2.6", + "vite-plugin-devtools-json": "^0.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.0.tgz", + "integrity": "sha512-ZBygRBqpDYiIHsN+d1WyHn3TYgzgpzLEcgJUxTATyiInQbKZz6wZb6+ljwdg8xeeOe4v03z6Uh6lELiw0/mVhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", + "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz", + "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz", + "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz", + "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz", + "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz", + "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz", + "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz", + "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz", + "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz", + "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz", + "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz", + "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz", + "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz", + "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz", + "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz", + "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz", + "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz", + "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz", + "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz", + "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz", + "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz", + "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.32.35", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.32.35.tgz", + "integrity": "sha512-Ul3YyOTU++to8cgNkttakC0dWvpERr6RYoHO2W47DLbFvrwBDJUY31B1sImH6JZSYc4Kt4PyHtoPNu+vL2r2dA==", + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.1.tgz", + "integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.22.0.tgz", + "integrity": "sha512-DJm0UxVgzXq+1MUfiJK4Ridk7oIQsIets6JwHiEl97sI6nXScfXe+BeqNhzB7jQIVBb3BM51U4hNk8qQxRXBAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0", + "vitefu": "^1.0.6" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", + "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", + "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz", + "integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz", + "integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-x64": "4.1.10", + "@tailwindcss/oxide-freebsd-x64": "4.1.10", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.10", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-x64-musl": "4.1.10", + "@tailwindcss/oxide-wasm32-wasi": "4.1.10", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz", + "integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz", + "integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz", + "integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz", + "integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz", + "integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz", + "integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz", + "integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz", + "integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz", + "integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz", + "integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.10", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz", + "integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz", + "integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.10.tgz", + "integrity": "sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.10", + "@tailwindcss/oxide": "4.1.10", + "tailwindcss": "4.1.10" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.35.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.9.3.tgz", + "integrity": "sha512-PlcyK80sqAZ43IITeZkgl3zPFWJytx/Joup9iKGqIOsXM2m3pWfPbWuXPr5PN3loXFEypqTY/JyZwNqlSpSvRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "esutils": "^2.0.3", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.9.tgz", + "integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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, + "license": "BSD-2-Clause", + "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, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "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.8" + }, + "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, + "license": "ISC", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.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, + "license": "MIT", + "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, + "license": "MIT", + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.1.tgz", + "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", + "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz", + "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.44.0", + "@rollup/rollup-android-arm64": "4.44.0", + "@rollup/rollup-darwin-arm64": "4.44.0", + "@rollup/rollup-darwin-x64": "4.44.0", + "@rollup/rollup-freebsd-arm64": "4.44.0", + "@rollup/rollup-freebsd-x64": "4.44.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", + "@rollup/rollup-linux-arm-musleabihf": "4.44.0", + "@rollup/rollup-linux-arm64-gnu": "4.44.0", + "@rollup/rollup-linux-arm64-musl": "4.44.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-gnu": "4.44.0", + "@rollup/rollup-linux-riscv64-musl": "4.44.0", + "@rollup/rollup-linux-s390x-gnu": "4.44.0", + "@rollup/rollup-linux-x64-gnu": "4.44.0", + "@rollup/rollup-linux-x64-musl": "4.44.0", + "@rollup/rollup-win32-arm64-msvc": "4.44.0", + "@rollup/rollup-win32-ia32-msvc": "4.44.0", + "@rollup/rollup-win32-x64-msvc": "4.44.0", + "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" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "5.34.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.8.tgz", + "integrity": "sha512-TF+8irl7rpj3+fpaLuPRX5BqReTAqckp0Fumxa/mCeK3fo0/MnBb9W/Z2bLwtqj3C3r5Lm6NKIAw7YrgIv1Fwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.8", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.2.tgz", + "integrity": "sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.2.0.tgz", + "integrity": "sha512-mbPtajIeuiyU80BEyGvwAktBeTX7KCr5/0l+uRGLq1dafwRNrjfM5kHGJScEBlPG3ipu6dJqfW/k0/fujvIEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", + "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie-file-store": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tough-cookie-file-store/-/tough-cookie-file-store-2.0.3.tgz", + "integrity": "sha512-sMpZVcmFf6EYFHFFl+SYH4W1/OnXBYMGDsv2IlbQ2caHyFElW/UR/gpj/KYU1JwmP4dE9xqwv2+vWcmlXHojSw==", + "license": "MIT", + "dependencies": { + "tough-cookie": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.35.0.tgz", + "integrity": "sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.35.0", + "@typescript-eslint/parser": "8.35.0", + "@typescript-eslint/utils": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.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, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-devtools-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-devtools-json/-/vite-plugin-devtools-json-0.2.0.tgz", + "integrity": "sha512-K7PoaWOEJECZ1n3VbhJXsUAX2PsO0xY7KFMM/Leh7tUev0M5zi+lz+vnVVdCK17IOK9Jp9rdzHXc08cnQirGbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "uuid": "^11.1.0" + }, + "peerDependencies": { + "vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.7.tgz", + "integrity": "sha512-eRWXLBbJjW3X5z5P5IHcSm2yYbYRPb2kQuc+oqsbAl99WB5kVsPbiiox+cymo8twTzifA6itvhr2CmjnaZZp0Q==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": 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, + "license": "ISC", + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yahoo-finance2": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-2.13.3.tgz", + "integrity": "sha512-ZECy6wQ7ymT08nVrxqQf+gwmINJ4/ECLyq+vM3SQmH3HWzje5DX1WX5YcZpWpWi4KXdmo2Vuk9OAdrTP09nE4g==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.32.27", + "@types/tough-cookie": "^4.0.2", + "tough-cookie": "^4.1.2", + "tough-cookie-file-store": "^2.0.3" + }, + "bin": { + "yahoo-finance": "bin/yahoo-finance.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/ping/frontend/package.json b/ping/frontend/package.json new file mode 100644 index 0000000..ac61dfe --- /dev/null +++ b/ping/frontend/package.json @@ -0,0 +1,44 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint ." + }, + "devDependencies": { + "@eslint/compat": "^1.2.5", + "@eslint/js": "^9.18.0", + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tailwindcss/vite": "^4.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^3.0.0", + "globals": "^16.0.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.11", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "typescript-eslint": "^8.20.0", + "vite": "^6.2.6", + "vite-plugin-devtools-json": "^0.2.0" + }, + "dependencies": { + "chart.js": "^4.5.0", + "yahoo-finance2": "^2.13.3" + } +} diff --git a/ping/frontend/src/app.css b/ping/frontend/src/app.css new file mode 100644 index 0000000..42e2b32 --- /dev/null +++ b/ping/frontend/src/app.css @@ -0,0 +1,58 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/forms'; +@plugin '@tailwindcss/typography'; + +:root { + --bg-primary: #1A1C22; + --bg-secondary: #282C35; + --btn-primary: #343844; + --btn-primary-hover: #2d8f3a; + --btn-secondary: #454b5a; + --text-lime: #00FF77; +} + +body { + background-color: var(--bg-secondary); +} + +input,select,textarea { + background-color: var(--bg-secondary); + border:none; + border-bottom: 2px solid var(--text-lime); + padding: 8px; + border-radius: 8px 8px 0 0; +} + +input[type="checkbox"], +input[type="radio"] { + accent-color: var(--text-lime); + border-radius: 8px; + border:none; +} + +.btn { + background-color: var(--btn-primary); + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + text-decoration: none; + transition-duration: 0.2s; + font-weight: 600; +} + +.btn:hover { + background-color: var(--btn-primary-hover); + transform: scale(1.025); +} + +.card { + background-color: var(--bg-primary); + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 16px; + color: white; + width: 100%; +} \ No newline at end of file diff --git a/ping/frontend/src/app.d.ts b/ping/frontend/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/ping/frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/ping/frontend/src/app.html b/ping/frontend/src/app.html new file mode 100644 index 0000000..e37ecf4 --- /dev/null +++ b/ping/frontend/src/app.html @@ -0,0 +1,13 @@ + + + + + + Patapimvest + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/ping/frontend/src/lib/components/Avatar.svelte b/ping/frontend/src/lib/components/Avatar.svelte new file mode 100644 index 0000000..c04b563 --- /dev/null +++ b/ping/frontend/src/lib/components/Avatar.svelte @@ -0,0 +1,48 @@ + + + +
+ + (showTooltip = true)} + onmouseleave={() => (showTooltip = false)} + {onclick} + alt="User Avatar" + /> + {#if showTooltip} +
{username}
+ {/if} +
+ + diff --git a/ping/frontend/src/lib/components/Button.svelte b/ping/frontend/src/lib/components/Button.svelte new file mode 100644 index 0000000..54b26ab --- /dev/null +++ b/ping/frontend/src/lib/components/Button.svelte @@ -0,0 +1,17 @@ + + +{#if href} + {@render children?.()} +{:else} + +{/if} diff --git a/ping/frontend/src/lib/components/NavBar.svelte b/ping/frontend/src/lib/components/NavBar.svelte new file mode 100644 index 0000000..1a76876 --- /dev/null +++ b/ping/frontend/src/lib/components/NavBar.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/ping/frontend/src/lib/components/NumberStatList.svelte b/ping/frontend/src/lib/components/NumberStatList.svelte new file mode 100644 index 0000000..b6444e8 --- /dev/null +++ b/ping/frontend/src/lib/components/NumberStatList.svelte @@ -0,0 +1,55 @@ + + +
+ {#each statsList as stat} +
+ + {stat.name} + {stat.value} +
+ {/each} +
+ + diff --git a/ping/frontend/src/lib/components/SideBar.svelte b/ping/frontend/src/lib/components/SideBar.svelte new file mode 100644 index 0000000..9ea8a04 --- /dev/null +++ b/ping/frontend/src/lib/components/SideBar.svelte @@ -0,0 +1,61 @@ + + + + + diff --git a/ping/frontend/src/lib/components/SteppedLineChart.svelte b/ping/frontend/src/lib/components/SteppedLineChart.svelte new file mode 100644 index 0000000..d393b44 --- /dev/null +++ b/ping/frontend/src/lib/components/SteppedLineChart.svelte @@ -0,0 +1,76 @@ + + +
+ +
diff --git a/ping/frontend/src/lib/components/ToastList.svelte b/ping/frontend/src/lib/components/ToastList.svelte new file mode 100644 index 0000000..5351ef7 --- /dev/null +++ b/ping/frontend/src/lib/components/ToastList.svelte @@ -0,0 +1,67 @@ + + +
+ {#each $toastList as toast} +
+

{toast.title}

+

{toast.message}

+
+ {/each} +
+ + diff --git a/ping/frontend/src/lib/components/UserItem.svelte b/ping/frontend/src/lib/components/UserItem.svelte new file mode 100644 index 0000000..44add60 --- /dev/null +++ b/ping/frontend/src/lib/components/UserItem.svelte @@ -0,0 +1,42 @@ + + +
+
+ + + +
+
+

Nom: {user.displayName}

+

Login: {user.login}

+

Role: {user.isAdmin ? 'ADMIN' : 'USER'}

+

Id: {user.id}

+
+
+ + diff --git a/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte b/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte new file mode 100644 index 0000000..3844abc --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/RiskAnalysis.svelte @@ -0,0 +1,10 @@ +

Analyse de risque

+tkt t safe c hardcodé chef + + diff --git a/ping/frontend/src/lib/components/dashboard/StockGraph.svelte b/ping/frontend/src/lib/components/dashboard/StockGraph.svelte new file mode 100644 index 0000000..beefed9 --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/StockGraph.svelte @@ -0,0 +1,136 @@ + + +
+
+

Vue d'ensemble : {selectedStock}

+
+ + + + + +
+
+ +
+ + diff --git a/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte b/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte new file mode 100644 index 0000000..3d947ad --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/TrendingSymbols.svelte @@ -0,0 +1,85 @@ + + +

Tendances

+ + + diff --git a/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte b/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte new file mode 100644 index 0000000..b9f0224 --- /dev/null +++ b/ping/frontend/src/lib/components/dashboard/transactions/TransactionModal.svelte @@ -0,0 +1,165 @@ + + + + +{#if isOpen} +
+ e.stopPropagation()}> +

Créer une nouvelle transaction

+
+ + + + + + + +
+
+
+{/if} + + diff --git a/ping/frontend/src/lib/components/input/StockSelector.svelte b/ping/frontend/src/lib/components/input/StockSelector.svelte new file mode 100644 index 0000000..1237128 --- /dev/null +++ b/ping/frontend/src/lib/components/input/StockSelector.svelte @@ -0,0 +1,231 @@ + + + + + +
+ + +
+
+
+ {#if stocks.length === 0} +

Aucune action, ETFs trouvés.

+ {:else} +

{stocks.length} actions, ETFs trouvés

+ {#each stocks as stock} + + {/each} + {/if} +
+
+ {#if news.length === 0} +

Pas de news trouvés.

+ {:else} +

{news.length} news trouvées

+ {#each news as newsItem} + + {#if newsItem.thumbnail?.resolutions?.length > 0} + News Thumbnail + {/if} +

{newsItem.title}

+ {/each} + {/if} +
+
+
+ + +
+ + diff --git a/ping/frontend/src/lib/components/input/UserSelector.svelte b/ping/frontend/src/lib/components/input/UserSelector.svelte new file mode 100644 index 0000000..a0c80c7 --- /dev/null +++ b/ping/frontend/src/lib/components/input/UserSelector.svelte @@ -0,0 +1,35 @@ + + +
+ {#each users as u, i} + { + users = users.filter((_, index) => index !== i); + }} + /> + {/each} + { + let userId = prompt("Entrez l'ID de l'utilisateur à ajouter :"); + if (userId === null || userId.trim() === '') { + addToast({ title: 'ID utilisateur invalide.' }); + return; + } + if (users.map((u) => u.displayName).includes(userId)) { + addToast({ title: 'Cet utilisateur est déjà ajouté.' }); + return; + } + users = [...users, { displayName: userId, avatar: '/img/default-avatar.png' } as IUser]; + }} + /> +
diff --git a/ping/frontend/src/lib/pages.ts b/ping/frontend/src/lib/pages.ts new file mode 100644 index 0000000..e5d424e --- /dev/null +++ b/ping/frontend/src/lib/pages.ts @@ -0,0 +1,54 @@ +interface SideBarItem { + icon: string; + name: string; + href: string; +} + +export const pages: SideBarItem[] = [ + { + name: 'Dashboard', + icon: '/icons/dashboard.svg', + href: '/dashboard' + }, + { + name: 'Transactions', + icon: '/icons/credit-card.svg', + href: '/dashboard/transactions' + }, + { + name: 'Modèles', + icon: '/icons/floppy-disk.svg', + href: '/dashboard/models' + }, + { + name: 'Analyses', + icon: '/icons/magnifying_glass_icon.svg', + href: '/dashboard/analyses' + }, + { + name: 'Personnel', + icon: '/icons/people.svg', + href: '/dashboard/personnel' + }, + { + name: 'Messages', + icon: '/icons/msg.svg', + href: '/dashboard/messages' + }, + { + name: 'Paramètres', + icon: '/icons/settings.svg', + href: '/dashboard/settings' + } +]; + +export function getPageIndex(pathname: string) { + if (pathname === '/dashboard') return 0; + if (pathname.startsWith('/dashboard/transactions')) return 1; + if (pathname.startsWith('/dashboard/models')) return 2; + if (pathname.startsWith('/dashboard/analyses')) return 3; + if (pathname.startsWith('/dashboard/personnel')) return 4; + if (pathname.startsWith('/dashboard/messages')) return 5; + if (pathname.startsWith('/dashboard/settings')) return 6; + return -1; // Not found +} \ No newline at end of file diff --git a/ping/frontend/src/lib/stores/auth.ts b/ping/frontend/src/lib/stores/auth.ts new file mode 100644 index 0000000..97acaf2 --- /dev/null +++ b/ping/frontend/src/lib/stores/auth.ts @@ -0,0 +1,66 @@ +import { error } from "@sveltejs/kit"; +import { get, writable } from "svelte/store"; + +export interface IUser { + id: string; + login: string; + displayName: string; + avatar: string; + isAdmin: boolean; +} + +export const user = writable(null); + +export async function getUser() { + const userVal = get(user); + if (userVal) { + return userVal; + } + return getUpdatedUser(); +} + +export async function getUpdatedUser() { + const token = localStorage.getItem("token"); + if (!token) { + localStorage.clear(); + window.location.replace("/login"); + throw new Error("Pas de token"); + } + try { + const userId = JSON.parse(atob(token.split(".")[1])).sub; + + const res = await authFetch(`/api/user/${userId}`); + if (!res.ok) { + throw error(res.status, "Erreur lors de la récupération de l'utilisateur"); + } + const data = await res.json(); + user.set({ ...data }); + return data; + } + catch (e) { + console.error("Erreur lors de la récupération de l'ID utilisateur depuis le token", e); + localStorage.clear(); + window.location.replace("/login"); + throw new Error("Token invalide " + e); + } +} + +// place files you want to import through the `$lib` alias in this folder. +export function authFetch(url: string | URL, options: RequestInit = {}) { + const token = localStorage.getItem("token"); + if (!token) { + localStorage.clear(); + window.location.replace("/login"); + throw new Error("Pas de token"); + } + + const mergedOptions: RequestInit = { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...(options.headers ?? {}) + } + }; + + return fetch(url, mergedOptions); +} \ No newline at end of file diff --git a/ping/frontend/src/lib/stores/toast.ts b/ping/frontend/src/lib/stores/toast.ts new file mode 100644 index 0000000..454461b --- /dev/null +++ b/ping/frontend/src/lib/stores/toast.ts @@ -0,0 +1,22 @@ +import { writable } from "svelte/store"; + +export interface Toast { + color?: string | undefined; + title?: string | undefined; + message?: string | undefined; +} + +export const toastList = writable([]); + +export function addToast(toast: Toast) { + toast ??= { color: "red", title: "Error", message: "An error occurred" }; + toast.color ??= "red"; + toast.title ??= "Error"; + toast.message ??= "An error occurred"; + + toastList.update((list) => [...list, toast]); + + setTimeout(() => { + toastList.update((list) => list.filter((t) => t !== toast)); + }, 5000); +} \ No newline at end of file diff --git a/ping/frontend/src/routes/+layout.svelte b/ping/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..ba995cc --- /dev/null +++ b/ping/frontend/src/routes/+layout.svelte @@ -0,0 +1,9 @@ + + +{@render children()} + diff --git a/ping/frontend/src/routes/+page.svelte b/ping/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..7088945 --- /dev/null +++ b/ping/frontend/src/routes/+page.svelte @@ -0,0 +1,59 @@ + + + + + {#snippet rightComponent()} + + {/snippet} + +
+
+
+
+

Investissons plus mieux et plus vert

+
    +
  • Investissez facilement
  • +
  • Analysez les risques et les performances
  • +
  • Soulagez votre éco-conscience
  • +
+
+
+ + diff --git a/ping/frontend/src/routes/dashboard/+layout.svelte b/ping/frontend/src/routes/dashboard/+layout.svelte new file mode 100644 index 0000000..a2f7efe --- /dev/null +++ b/ping/frontend/src/routes/dashboard/+layout.svelte @@ -0,0 +1,44 @@ + + + + + {#snippet rightComponent()} +
+
+ +
+ +
+ {/snippet} +
+ +
{@render children()}
+ + diff --git a/ping/frontend/src/routes/dashboard/+page.svelte b/ping/frontend/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..c38e281 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/+page.svelte @@ -0,0 +1,81 @@ + + +
+ +
+
+ +
+ + +
+ +
+ +
+
+ + diff --git a/ping/frontend/src/routes/dashboard/analyses/+page.svelte b/ping/frontend/src/routes/dashboard/analyses/+page.svelte new file mode 100644 index 0000000..1a8ed47 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/analyses/+page.svelte @@ -0,0 +1,409 @@ + + +
+
+

Analyses des Transactions

+
+ + +
+
+ + + + {#if analysisData.thresholdAlerts} +
+ {#if analysisData.thresholdAlerts.dailySpending} +
+ ⚠️ Dépenses quotidiennes élevées ({(Math.abs(analysisData.totalAmount) / selectedPeriod).toFixed(0)}€/jour) +
+ {/if} + {#if analysisData.thresholdAlerts.co2Impact} +
+ 🌍 Impact CO2 élevé ({analysisData.totalCO2}g) +
+ {/if} + {#if analysisData.thresholdAlerts.profitTarget} +
+ 🎯 Objectif de profit atteint ({analysisData.totalAmount.toFixed(0)}€) +
+ {/if} +
+ {/if} + +
+
+

Transactions dans le temps

+ {#if analysisData.chartData?.length > 0} + + {:else} +

Aucune donnée

+ {/if} +
+ +
+

Top destinataires

+
+ {#each (analysisData.recipientChartData || []).slice(0, 5) as recipient} +
+ {recipient.x} +
+ {#if analysisData.recipientChartData?.length > 0} + {@const maxValue = Math.max(...analysisData.recipientChartData.map(r => r.y))} +
+ {:else} +
+ {/if} + {recipient.y.toFixed(0)}€ +
+
+ {/each} +
+
+ +
+

Répartition des transactions

+
+
+ Achats: + + {(analysisData.filteredTransactions || []).filter((t: Transaction) => t.type === 'buy').length} + +
+
+ Ventes: + + {(analysisData.filteredTransactions || []).filter((t: Transaction) => t.type === 'sell').length} + +
+
+ Volume total: + + {Math.abs(analysisData.totalAmount || 0).toFixed(0)}€ + +
+
+
+ +
+

Métriques CO2

+
+
+ Impact total: + + {analysisData.totalCO2 || 0}g CO2 + +
+
+ Par transaction: + + {(analysisData.filteredTransactions?.length ? analysisData.totalCO2 / analysisData.filteredTransactions.length : 0).toFixed(1)}g + +
+
+
+
+
+ + diff --git a/ping/frontend/src/routes/dashboard/messages/+page.svelte b/ping/frontend/src/routes/dashboard/messages/+page.svelte new file mode 100644 index 0000000..52869e4 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/messages/+page.svelte @@ -0,0 +1,403 @@ + + +
+
+ +
+

Messages

+ +
+ +
+ +
+

Boîte de réception

+
+ {#each messages as message} +
selectMessage(message)} + > +
+ {message.sender} + {message.date.toLocaleDateString()} +
+
{message.subject}
+
{message.content.substring(0, 80)}...
+ {#if message.unread} +
+ {/if} +
+ {/each} +
+
+ + +
+ {#if selectedMessage} +
+

{selectedMessage.subject}

+
+ +
+
+
+ De: {selectedMessage.sender} + Date: {selectedMessage.date.toLocaleDateString()} +
+
+ {selectedMessage.content} +
+ + +
+

Répondre

+ + +
+ {:else} +
+

Sélectionnez un message

+

Choisissez un message dans la liste pour le lire

+
+ {/if} +
+
+
+
+ + diff --git a/ping/frontend/src/routes/dashboard/models/+page.svelte b/ping/frontend/src/routes/dashboard/models/+page.svelte new file mode 100644 index 0000000..3be6f00 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/models/+page.svelte @@ -0,0 +1,568 @@ + + +
+
+
+

Modèles actifs

+
+ {#each models as m, i} +
+
+ {m.name}
+ {m.id} +
+
+
+ Eco score + {69} % +
+
+ Efficacité + {69} % +
+
+ + +
+ {/each} +
+
+ +
+
+
+ {#if modelToEdit !== null} +

Modifier un modèle

+ {:else} +

Nouveau modèle

+ {/if} + +
+
+ + +
+ {#if modelToEdit !== null} + + {:else if currentUser} + + {:else} + Chargement en cours... + {/if} + +
+
+ +
+
+ Sources de données + +
+ + {#each sources as source, index} +
+ + +
+ + +
+
+ {/each} +
+
+
+
+
+ + diff --git a/ping/frontend/src/routes/dashboard/personnel/+page.svelte b/ping/frontend/src/routes/dashboard/personnel/+page.svelte new file mode 100644 index 0000000..a1722c1 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/personnel/+page.svelte @@ -0,0 +1,290 @@ + + +{#if user} +
+
+
+

Mon compte

+ +

Nom: {user.displayName}

+

Login: {user.login}

+

Role: {user.isAdmin ? 'ADMIN' : 'USER'}

+

Id: {user.id}

+
+
+

Modifier

+ Laisser vide pour ne pas modifier
+ {#if user.isAdmin} +

Vous pouvez modifier les informations de n'importe quel utilisateur.

+ {:else} +

Vous ne pouvez modifier que vos propres informations.

+ {/if} +

+
+ {#if user.isAdmin} + + + {/if} + + + + + + + +
+
+
+ + {#if user.isAdmin} +
+
+

Créer un nouvel utilisateur

+
+ + + + + + + + + +
+
+
+

Liste des utilisateurs

+
+ {#each allUsers as u} + { + idValue = u.id; + urlValue = u.avatar || ''; + nameValue = u.displayName || ''; + }} + onDelete={() => { + authFetch(`/api/user/${u.id}`, { + method: 'DELETE' + }) + .then(async (res) => { + if (!res.ok) { + addToast({ + title: 'Erreur', + message: "Impossible de supprimer l'utilisateur." + }); + throw new Error("Erreur lors de la suppression de l'utilisateur"); + } + allUsers = allUsers.filter((user) => user.id !== u.id); + addToast({ + title: 'Succès', + message: 'Utilisateur supprimé avec succès.', + color: 'green' + }); + }) + .catch(() => { + addToast({ + title: 'Erreur', + message: "Impossible de supprimer l'utilisateur." + }); + }); + }} + /> + {/each} +
+
+
+ {/if} +
+{:else} +

Chargement des informations utilisateur...

+{/if} + + diff --git a/ping/frontend/src/routes/dashboard/settings/+page.svelte b/ping/frontend/src/routes/dashboard/settings/+page.svelte new file mode 100644 index 0000000..99c5d8f --- /dev/null +++ b/ping/frontend/src/routes/dashboard/settings/+page.svelte @@ -0,0 +1,452 @@ + + +
+
+
+

Paramètres

+
+ +
+
+

👤 Profil utilisateur

+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+

🔔 Notifications

+
+
+ + + + + +
+ +
+
+ +
+

🔒 Sécurité

+
+
+ +
+
+ + +
+
+ +
+ +
+ +
+

Changer le mot de passe

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+

🎨 Affichage

+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+

📊 Gestion des données

+
+
+
+

Exporter mes données

+

Téléchargez toutes vos données personnelles dans un fichier JSON

+
+ +
+
+
+

Supprimer mon compte

+

Supprimez définitivement votre compte et toutes vos données

+
+ +
+
+
+
+
+
+ + diff --git a/ping/frontend/src/routes/dashboard/transactions/+page.svelte b/ping/frontend/src/routes/dashboard/transactions/+page.svelte new file mode 100644 index 0000000..86f9292 --- /dev/null +++ b/ping/frontend/src/routes/dashboard/transactions/+page.svelte @@ -0,0 +1,360 @@ + + +
+
+
+

Transactions

+ +
+ +
+
+ Trier +
+ + +
+
+
+ + +
+
+ +
+ {#each visibleTransactions as transaction} +
+
+
+

{transaction.label}

+

+ {transaction.receiverIban} • {transaction.receiverLabel} +

+
+
+
+ CO2 + {transaction.eco_score} +
+
+ {transaction.currency} + {transaction.amount} +
+
+ +
+
+ {/each} +
+
+
+ + + diff --git a/ping/frontend/src/routes/login/+page.svelte b/ping/frontend/src/routes/login/+page.svelte new file mode 100644 index 0000000..0d66f8d --- /dev/null +++ b/ping/frontend/src/routes/login/+page.svelte @@ -0,0 +1,149 @@ + + +
+
+

Connectez-vous

+
+ + + + + +
+
+
+ + + diff --git a/ping/frontend/src/routes/stocksapi/chart/+server.ts b/ping/frontend/src/routes/stocksapi/chart/+server.ts new file mode 100644 index 0000000..3271e83 --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/chart/+server.ts @@ -0,0 +1,21 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/chart?query=AAPL&startDate=2025-01-01& + */ +export const GET: RequestHandler = async ({ request }) => { + const sp = new URL(request.url).searchParams; + const query = sp.get('query') || 'AAPL'; + const startDate = sp.get('startDate') || '2025-01-01'; + const endDate = sp.get('endDate') || new Date().toISOString().split('T')[0]; + const interval = sp.get('interval') || '1d'; + + const data = await yahooFinance.chart(query, { period1: startDate, period2: endDate, interval: interval }); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +}; \ No newline at end of file diff --git a/ping/frontend/src/routes/stocksapi/insights/+server.ts b/ping/frontend/src/routes/stocksapi/insights/+server.ts new file mode 100644 index 0000000..6be76e9 --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/insights/+server.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/insights?query=AAPL + */ +export const GET: RequestHandler = async ({ request }) => { + const sp = new URL(request.url).searchParams; + const query = sp.get('query') || 'AAPL'; + + const queryOptions = { lang: 'en-US', reportsCount: 2, region: 'US' }; + const data = await yahooFinance.insights(query, queryOptions); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +}; \ No newline at end of file diff --git a/ping/frontend/src/routes/stocksapi/quote/+server.ts b/ping/frontend/src/routes/stocksapi/quote/+server.ts new file mode 100644 index 0000000..96619fc --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/quote/+server.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/quote?query=AAPL + */ +export const GET: RequestHandler = async ({ request }) => { + const sp = new URL(request.url).searchParams; + const query = sp.get('query') || 'AAPL'; + + const data = await yahooFinance.quote(query); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +}; \ No newline at end of file diff --git a/ping/frontend/src/routes/stocksapi/search/+server.ts b/ping/frontend/src/routes/stocksapi/search/+server.ts new file mode 100644 index 0000000..e58fc6b --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/search/+server.ts @@ -0,0 +1,18 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/search?query=AAPL + */ +export const GET: RequestHandler = async ({ request }) => { + const sp = new URL(request.url).searchParams; + const query = sp.get('query') || 'AAPL'; + + const data = await yahooFinance.search(query, { region: 'US' }) + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +}; \ No newline at end of file diff --git a/ping/frontend/src/routes/stocksapi/trendingSymbols/+server.ts b/ping/frontend/src/routes/stocksapi/trendingSymbols/+server.ts new file mode 100644 index 0000000..3dac574 --- /dev/null +++ b/ping/frontend/src/routes/stocksapi/trendingSymbols/+server.ts @@ -0,0 +1,16 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import yahooFinance from 'yahoo-finance2'; + +/** + * Example request : GET /stocksapi/trendingSymbols + */ +export const GET: RequestHandler = async () => { + const queryOptions = { count: 5, lang: 'en-US' }; + const data = await yahooFinance.trendingSymbols('US', queryOptions); + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + }, + }); +}; \ No newline at end of file diff --git a/ping/frontend/static/favicon.png b/ping/frontend/static/favicon.png new file mode 100644 index 0000000..83e6bff Binary files /dev/null and b/ping/frontend/static/favicon.png differ diff --git a/ping/frontend/static/icons/ICI b/ping/frontend/static/icons/ICI new file mode 100644 index 0000000..01a9115 --- /dev/null +++ b/ping/frontend/static/icons/ICI @@ -0,0 +1,2 @@ +todo : remove this file +fact: les triangles sont pas exactement les memes diff --git a/ping/frontend/static/icons/add-green.svg b/ping/frontend/static/icons/add-green.svg new file mode 100644 index 0000000..d165f30 --- /dev/null +++ b/ping/frontend/static/icons/add-green.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/bell.svg b/ping/frontend/static/icons/bell.svg new file mode 100644 index 0000000..797220a --- /dev/null +++ b/ping/frontend/static/icons/bell.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/closed-wallet.svg b/ping/frontend/static/icons/closed-wallet.svg new file mode 100644 index 0000000..ffd10f6 --- /dev/null +++ b/ping/frontend/static/icons/closed-wallet.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/coin.svg b/ping/frontend/static/icons/coin.svg new file mode 100644 index 0000000..331f0aa --- /dev/null +++ b/ping/frontend/static/icons/coin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/credit-card.svg b/ping/frontend/static/icons/credit-card.svg new file mode 100644 index 0000000..d84911c --- /dev/null +++ b/ping/frontend/static/icons/credit-card.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/dashboard.svg b/ping/frontend/static/icons/dashboard.svg new file mode 100644 index 0000000..072ca90 --- /dev/null +++ b/ping/frontend/static/icons/dashboard.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/debug.svg b/ping/frontend/static/icons/debug.svg new file mode 100644 index 0000000..4f6cfb2 --- /dev/null +++ b/ping/frontend/static/icons/debug.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/edit-pen.svg b/ping/frontend/static/icons/edit-pen.svg new file mode 100644 index 0000000..0c6f108 --- /dev/null +++ b/ping/frontend/static/icons/edit-pen.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/floppy-disk.svg b/ping/frontend/static/icons/floppy-disk.svg new file mode 100644 index 0000000..4952daf --- /dev/null +++ b/ping/frontend/static/icons/floppy-disk.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/leaf.svg b/ping/frontend/static/icons/leaf.svg new file mode 100644 index 0000000..d58987d --- /dev/null +++ b/ping/frontend/static/icons/leaf.svg @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/magnifying_glass_icon.svg b/ping/frontend/static/icons/magnifying_glass_icon.svg new file mode 100644 index 0000000..dd521f6 --- /dev/null +++ b/ping/frontend/static/icons/magnifying_glass_icon.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/money-bills.svg b/ping/frontend/static/icons/money-bills.svg new file mode 100644 index 0000000..57b37a7 --- /dev/null +++ b/ping/frontend/static/icons/money-bills.svg @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/msg.svg b/ping/frontend/static/icons/msg.svg new file mode 100644 index 0000000..6f3930f --- /dev/null +++ b/ping/frontend/static/icons/msg.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/people.svg b/ping/frontend/static/icons/people.svg new file mode 100644 index 0000000..b8ad20a --- /dev/null +++ b/ping/frontend/static/icons/people.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/settings.svg b/ping/frontend/static/icons/settings.svg new file mode 100644 index 0000000..25f7ec8 --- /dev/null +++ b/ping/frontend/static/icons/settings.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/side-menu.svg b/ping/frontend/static/icons/side-menu.svg new file mode 100644 index 0000000..5972d63 --- /dev/null +++ b/ping/frontend/static/icons/side-menu.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/trash.svg b/ping/frontend/static/icons/trash.svg new file mode 100644 index 0000000..e19d236 --- /dev/null +++ b/ping/frontend/static/icons/trash.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/triangle-down.svg b/ping/frontend/static/icons/triangle-down.svg new file mode 100644 index 0000000..9a40495 --- /dev/null +++ b/ping/frontend/static/icons/triangle-down.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/triangle-up.svg b/ping/frontend/static/icons/triangle-up.svg new file mode 100644 index 0000000..0b54139 --- /dev/null +++ b/ping/frontend/static/icons/triangle-up.svg @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/icons/wallet.svg b/ping/frontend/static/icons/wallet.svg new file mode 100644 index 0000000..7dfd46f --- /dev/null +++ b/ping/frontend/static/icons/wallet.svg @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/img/default-avatar.png b/ping/frontend/static/img/default-avatar.png new file mode 100644 index 0000000..406d770 Binary files /dev/null and b/ping/frontend/static/img/default-avatar.png differ diff --git a/ping/frontend/static/img/favicon.svg b/ping/frontend/static/img/favicon.svg new file mode 100644 index 0000000..3d7b3eb --- /dev/null +++ b/ping/frontend/static/img/favicon.svg @@ -0,0 +1,29 @@ + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/img/header-bg.jpg b/ping/frontend/static/img/header-bg.jpg new file mode 100644 index 0000000..ca76595 Binary files /dev/null and b/ping/frontend/static/img/header-bg.jpg differ diff --git a/ping/frontend/static/img/logo.svg b/ping/frontend/static/img/logo.svg new file mode 100644 index 0000000..c73d1fe --- /dev/null +++ b/ping/frontend/static/img/logo.svg @@ -0,0 +1,41 @@ + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/static/img/logoFull.svg b/ping/frontend/static/img/logoFull.svg new file mode 100644 index 0000000..505564a --- /dev/null +++ b/ping/frontend/static/img/logoFull.svg @@ -0,0 +1,75 @@ + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ping/frontend/svelte.config.js b/ping/frontend/svelte.config.js new file mode 100644 index 0000000..1295460 --- /dev/null +++ b/ping/frontend/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/ping/frontend/tsconfig.json b/ping/frontend/tsconfig.json new file mode 100644 index 0000000..0b2d886 --- /dev/null +++ b/ping/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/ping/frontend/vite.config.ts b/ping/frontend/vite.config.ts new file mode 100644 index 0000000..ef26a3a --- /dev/null +++ b/ping/frontend/vite.config.ts @@ -0,0 +1,30 @@ +import tailwindcss from '@tailwindcss/vite'; +import devtoolsJson from 'vite-plugin-devtools-json'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig, loadEnv } from 'vite'; + +export default defineConfig(({ mode }) => { + // @ts-expect-error process is loaded + const env = loadEnv(mode, process.cwd()); + + console.log("Found API URL:", env.VITE_BACKEND_URL); + + const API_URL = (env.VITE_BACKEND_URL ?? "http://127.0.1:8080"); + + return { + plugins: [tailwindcss(), sveltekit(), devtoolsJson()], + optimizeDeps: { + exclude: ['chart.js'] + }, + server: { + proxy: { + '/api': API_URL + }, + allowedHosts: [ + 'patapimvest.esteban-charvin.dev', + 'localhost', + '127.0.0.1' + ], + } + } +}); diff --git a/ping/ping/.env b/ping/ping/.env new file mode 100644 index 0000000..4808f47 --- /dev/null +++ b/ping/ping/.env @@ -0,0 +1,3 @@ +PROJECT_DEFAULT_PATH = ping +LOG_FILE=ping.log +ERROR_LOG_FILE=error.log diff --git a/ping/ping/pom.xml b/ping/ping/pom.xml new file mode 100644 index 0000000..cf9c1e3 --- /dev/null +++ b/ping/ping/pom.xml @@ -0,0 +1,307 @@ + + + 4.0.0 + fr.epita.assistants + ping + 1.0 + + + 3.13.0 + + 21 + 21 + 21 + + 3.13.0 + 3.2.5 + 3.4.1 + 3.1.2 + + UTF-8 + UTF-8 + + quarkus-bom + io.quarkus.platform + 3.17.5 + + true + 3.2.5 + + 9.2.0 + 1.18.30 + 6.1.0.202203080745-r + 1.21 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-resteasy-jsonb + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + test + + + + org.apache.maven.surefire + surefire-junit-platform + ${versions.maven-surefire-plugin} + + + + io.rest-assured + rest-assured + test + + + + org.eclipse.jgit + org.eclipse.jgit + ${versions.jgit} + + + + com.fasterxml.jackson.core + jackson-databind + 2.16.0 + + + + org.apache.lucene + lucene-core + ${versions.lucene} + + + + org.apache.lucene + lucene-queryparser + ${versions.lucene} + + + + javax.validation + validation-api + 2.0.1.Final + + + + org.projectlombok + lombok + ${versions.lombok} + true + + + + org.assertj + assertj-core + 3.24.2 + + + + com.tngtech.archunit + archunit-junit5 + 1.3.0 + + + + commons-io + commons-io + 2.6 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + 2.16.0 + + + com.fasterxml.jackson.core + jackson-core + 2.16.0 + + + + org.yaml + snakeyaml + 2.0 + + + + javax.servlet + servlet-api + 2.5 + + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-jdbc-postgresql + + + + + io.quarkus + quarkus-smallrye-openapi + + + + io.quarkus + quarkus-resteasy-jackson + + + + io.quarkus + quarkus-smallrye-jwt-build + + + io.quarkus + quarkus-smallrye-jwt + + + + com.auth0 + java-jwt + 4.4.0 + test + + + + + + org.junit.jupiter + junit-jupiter + 5.9.1 + test + + + org.junit.platform + junit-platform-launcher + 1.9.1 + + + io.quarkus + quarkus-junit5-mockito + test + + + io.quarkus + quarkus-test-security-jwt + test + + + + + + + org.apache.maven.plugins + maven-clean-plugin + 2.5 + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + + org.apache.maven.plugins + maven-dependency-plugin + 3.3.0 + + + maven-assembly-plugin + + + + fully.qualified.MainClass + + + + jar-with-dependencies + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.0 + + + + + + + native + + + native + + + + false + true + + + + diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/ExecFeatureRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/ExecFeatureRequest.java new file mode 100644 index 0000000..50f665c --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/ExecFeatureRequest.java @@ -0,0 +1,20 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.lang.reflect.Array; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class ExecFeatureRequest +{ + public String feature; + public String command; + public List params; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/LoginRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/LoginRequest.java new file mode 100644 index 0000000..c88dc2c --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/LoginRequest.java @@ -0,0 +1,16 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class LoginRequest +{ + public String login; + public String password; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/MoveRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/MoveRequest.java new file mode 100644 index 0000000..260d574 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/MoveRequest.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class MoveRequest +{ + public String src; + public String dst; + +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewProjectRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewProjectRequest.java new file mode 100644 index 0000000..8033ecb --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewProjectRequest.java @@ -0,0 +1,15 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class NewProjectRequest +{ + public String name; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewTransactionRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewTransactionRequest.java new file mode 100644 index 0000000..f7a3c6b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewTransactionRequest.java @@ -0,0 +1,21 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class NewTransactionRequest { + + public Float amount; + + public String currency; + + public String label = null; + + public String receiverLabel = null; + + public String receiverIban; + + public LocalDateTime operationDate; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewUserRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewUserRequest.java new file mode 100644 index 0000000..9023b91 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/NewUserRequest.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class NewUserRequest +{ + public String login; + public String password; + public Boolean isAdmin; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/PathRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/PathRequest.java new file mode 100644 index 0000000..a5fcb36 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/PathRequest.java @@ -0,0 +1,15 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class PathRequest +{ + public String relativePath = ""; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateProjectRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateProjectRequest.java new file mode 100644 index 0000000..94f872d --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateProjectRequest.java @@ -0,0 +1,16 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UpdateProjectRequest +{ + public String name; + public String newOwnerId; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateTransactionRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateTransactionRequest.java new file mode 100644 index 0000000..58b90d0 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateTransactionRequest.java @@ -0,0 +1,8 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class UpdateTransactionRequest extends NewTransactionRequest {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateUserRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateUserRequest.java new file mode 100644 index 0000000..5bb4176 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UpdateUserRequest.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UpdateUserRequest +{ + public String password; + public String displayName; + public String avatar; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UserProjectRequest.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UserProjectRequest.java new file mode 100644 index 0000000..9f30b84 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/requests/UserProjectRequest.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.requests; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UserProjectRequest +{ + // WARNING: THE ID FIELD ID A UUID STRING + // MAKE SURE THAT IT PASSES THE REGEX SHOWN IN THE SWAGGER + public String userId; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/EcoScoreResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/EcoScoreResponse.java new file mode 100644 index 0000000..129f63c --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/EcoScoreResponse.java @@ -0,0 +1,9 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.Data; + +@Data +public class EcoScoreResponse { + + public final Float score; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/FSEntryResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/FSEntryResponse.java new file mode 100644 index 0000000..e3443ed --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/FSEntryResponse.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class FSEntryResponse +{ + public String name; + public String path; + public Boolean isDirectory; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/LoginResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/LoginResponse.java new file mode 100644 index 0000000..b59ede0 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/LoginResponse.java @@ -0,0 +1,15 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class LoginResponse +{ + public String token; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/ProjectResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/ProjectResponse.java new file mode 100644 index 0000000..100b996 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/ProjectResponse.java @@ -0,0 +1,22 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class ProjectResponse +{ + // WARNING: THE ID FIELD ID A UUID STRING + // MAKE SURE THAT IT PASSES THE REGEX SHOWN IN THE SWAGGER + public String id; + public String name; + public List members; + public UserSummaryResponse owner; +} \ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionResponse.java new file mode 100644 index 0000000..51d07f6 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionResponse.java @@ -0,0 +1,30 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +public class TransactionResponse { + + public final UUID id; + + public final String label; + + public final Float amount; + + public final String currency; + + public final String receiverLabel; + + public final String receiverIban; + + public final LocalDateTime operationDate; + + public final Float eco_score; + + public final LocalDateTime creationDate; + + public final UUID createrId; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionsResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionsResponse.java new file mode 100644 index 0000000..03efb1e --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/TransactionsResponse.java @@ -0,0 +1,11 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.Data; + +import java.util.List; + +@Data +public class TransactionsResponse { + + public final List transactions; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserResponse.java new file mode 100644 index 0000000..04c2a57 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserResponse.java @@ -0,0 +1,23 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.UUID; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UserResponse +{ + // WARNING: THE ID FIELD ID A UUID STRING + // MAKE SURE THAT IT PASSES THE REGEX SHOWN IN THE SWAGGER + public UUID id; + public String login; + public String displayName; + public Boolean isAdmin; + public String avatar; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserSummaryResponse.java b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserSummaryResponse.java new file mode 100644 index 0000000..228ebc7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/api/responses/UserSummaryResponse.java @@ -0,0 +1,21 @@ +package fr.epita.assistants.ping.api.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +// The only purpose of this class is to be used by +// the ProjectResponse class +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UserSummaryResponse +{ + // WARNING: THE ID FIELD ID A UUID STRING + // MAKE SURE THAT IT PASSES THE REGEX SHOWN IN THE SWAGGER + public String id; + public String displayName; + public String avatar; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/IConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/IConverter.java new file mode 100644 index 0000000..5db6e3b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/IConverter.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.converters; + + +import java.util.List; + +// Maxence regarde j'ai écrit Teto :D +public interface IConverter { + + TTo convert(TFrom object); + + default List convert(List objects) { + return objects + .stream() + .map(this::convert) + .toList(); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionCreationEntityConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionCreationEntityConverter.java new file mode 100644 index 0000000..99372b3 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionCreationEntityConverter.java @@ -0,0 +1,23 @@ +package fr.epita.assistants.ping.converters.request; + +import fr.epita.assistants.ping.api.requests.NewTransactionRequest; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.domain.entity.TransactionCreationEntity; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.Currency; + +@ApplicationScoped +public class TransactionCreationEntityConverter implements IConverter { + + public TransactionCreationEntity convert(final NewTransactionRequest request) throws IllegalArgumentException { + return new TransactionCreationEntity( + request.getAmount(), + Currency.getInstance(request.getCurrency()), + request.getLabel(), + request.getReceiverLabel(), + request.getReceiverIban(), + request.getOperationDate() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionUpdateEntityConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionUpdateEntityConverter.java new file mode 100644 index 0000000..1448716 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/TransactionUpdateEntityConverter.java @@ -0,0 +1,23 @@ +package fr.epita.assistants.ping.converters.request; + +import fr.epita.assistants.ping.api.requests.UpdateTransactionRequest; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.domain.entity.TransactionUpdateEntity; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.Currency; + +@ApplicationScoped +public class TransactionUpdateEntityConverter implements IConverter { + + public TransactionUpdateEntity convert(final UpdateTransactionRequest request) throws IllegalArgumentException { + return new TransactionUpdateEntity( + request.getAmount(), + Currency.getInstance(request.getCurrency()), + request.getLabel(), + request.getReceiverLabel(), + request.getReceiverIban(), + request.getOperationDate() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserCreationEntityConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserCreationEntityConverter.java new file mode 100644 index 0000000..b452d9e --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserCreationEntityConverter.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.converters.request; + +import fr.epita.assistants.ping.api.requests.NewUserRequest; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.domain.entity.UserCreationEntity; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class UserCreationEntityConverter implements IConverter { + + public UserCreationEntity convert(final NewUserRequest request) { + return new UserCreationEntity( + request.getLogin(), + request.getPassword(), + request.getIsAdmin() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserUpdateEntityConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserUpdateEntityConverter.java new file mode 100644 index 0000000..3803a97 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/request/UserUpdateEntityConverter.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.converters.request; + +import fr.epita.assistants.ping.api.requests.UpdateUserRequest; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.domain.entity.UserUpdateEntity; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class UserUpdateEntityConverter implements IConverter { + + public UserUpdateEntity convert(final UpdateUserRequest request) { + return new UserUpdateEntity( + request.getPassword(), + request.getDisplayName(), + request.getAvatar() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/ProjectResponseConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/ProjectResponseConverter.java new file mode 100644 index 0000000..b45c764 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/ProjectResponseConverter.java @@ -0,0 +1,25 @@ +package fr.epita.assistants.ping.converters.response; + +import fr.epita.assistants.ping.api.responses.ProjectResponse; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.data.model.ProjectModel; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class ProjectResponseConverter implements IConverter { + + @Inject + UserSummaryResponseConverter userSummaryResponseConverter; + + public ProjectResponse convert(ProjectModel p) { + var owner = p.getOwner(); + + return new ProjectResponse( + p.getId().toString(), + p.getName(), + p.getMembers().stream().map(userSummaryResponseConverter::convert).toList(), + userSummaryResponseConverter.convert(owner) + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/TransactionResponseConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/TransactionResponseConverter.java new file mode 100644 index 0000000..daed47b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/TransactionResponseConverter.java @@ -0,0 +1,25 @@ +package fr.epita.assistants.ping.converters.response; + +import fr.epita.assistants.ping.api.responses.TransactionResponse; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.data.model.TransactionModel; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class TransactionResponseConverter implements IConverter { + + public TransactionResponse convert(TransactionModel transactionModel) { + return new TransactionResponse( + transactionModel.getId(), + transactionModel.getLabel(), + transactionModel.getAmount(), + transactionModel.getCurrency().getCurrencyCode(), + transactionModel.getReceiverLabel(), + transactionModel.getReceiverIban(), + transactionModel.getOperationDate(), + transactionModel.getEcoScore(), + transactionModel.getCreationTimestamp(), + transactionModel.getCreationUser().getId() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserResponseConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserResponseConverter.java new file mode 100644 index 0000000..ef251d0 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserResponseConverter.java @@ -0,0 +1,21 @@ +package fr.epita.assistants.ping.converters.response; + +import fr.epita.assistants.ping.api.responses.UserResponse; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.data.model.UserModel; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class UserResponseConverter implements IConverter { + + public UserResponse convert(final UserModel userModel) { + return new UserResponse( + userModel.getId(), + userModel.getLogin(), + userModel.getDisplayName(), + userModel.getIsAdmin(), + userModel.getAvatar() + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserSummaryResponseConverter.java b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserSummaryResponseConverter.java new file mode 100644 index 0000000..aa4d1a4 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/converters/response/UserSummaryResponseConverter.java @@ -0,0 +1,20 @@ +package fr.epita.assistants.ping.converters.response; + +import fr.epita.assistants.ping.api.responses.UserSummaryResponse; +import fr.epita.assistants.ping.converters.IConverter; +import fr.epita.assistants.ping.data.model.UserModel; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class UserSummaryResponseConverter implements IConverter { + + public UserSummaryResponse convert(UserModel um) { + return new UserSummaryResponse( + um.getId().toString(), + um.getDisplayName(), + um.getAvatar() + ); + } + +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/ProjectModel.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/ProjectModel.java new file mode 100644 index 0000000..f969cf7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/ProjectModel.java @@ -0,0 +1,43 @@ +package fr.epita.assistants.ping.data.model; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; +import java.util.UUID; + +@Getter +@Setter +@Entity +@NoArgsConstructor +@AllArgsConstructor +@ToString +@Table(name = "projects") +public class ProjectModel { + // TODO DONE + @Id + @GeneratedValue(strategy = GenerationType.UUID) + UUID id; + @ManyToOne + @With + @JoinColumn(name = "owner_id", referencedColumnName = "id") + UserModel owner; + @With + String name; + @With + String path; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name="project_members", + joinColumns = @JoinColumn(name = "project_id"), + inverseJoinColumns = @JoinColumn(name = "user_id") + ) + List members; + + public boolean hasMember(final UserModel user) { + return owner.getId().compareTo(user.getId()) == 0 || members + .stream() + .anyMatch(userModel -> userModel.getId().compareTo(user.getId()) == 0); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/TransactionModel.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/TransactionModel.java new file mode 100644 index 0000000..08289f7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/TransactionModel.java @@ -0,0 +1,61 @@ +package fr.epita.assistants.ping.data.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.Currency; +import java.util.UUID; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "transactions") +public class TransactionModel extends PanacheEntityBase { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + UUID id; + + @With + String label; + + @With + Float amount; + + @With + Currency currency; + + @With + @Nullable + @Column(name = "receiver_label") + String receiverLabel = null; + + @With + @Column(name = "receiver_iban") + String receiverIban; + + @With + @Column(name = "operation_date") + LocalDateTime operationDate; + + @With + @Column(name = "eco_score") + Float ecoScore; + + @With + @ManyToOne + @JoinColumn(name = "creation_user") + UserModel creationUser; + + @Column(name = "creation_timestamp") + LocalDateTime creationTimestamp = LocalDateTime.now(); + + @Nullable + @Column(name = "update_timestamp") + LocalDateTime updateTimestamp = null; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/UserModel.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/UserModel.java new file mode 100644 index 0000000..07a54c8 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/model/UserModel.java @@ -0,0 +1,42 @@ +package fr.epita.assistants.ping.data.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Getter +@Setter +@Entity +@NoArgsConstructor +@AllArgsConstructor +@ToString +@Table(name = "users") +public class UserModel { + // TODO DONE + @Id @GeneratedValue(strategy = GenerationType.UUID) + UUID id; + + @Column(name = "display_name") @With + String displayName; + + @Column(name = "is_admin") @With + Boolean isAdmin; + + @With + String avatar; + + @With + String login; + + @With + String password; + + @JsonIgnore + @ToString.Exclude + @ManyToMany(mappedBy = "members", fetch = FetchType.EAGER) + List projectMemberships; +} \ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/ProjectRepository.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/ProjectRepository.java new file mode 100644 index 0000000..31346f0 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/ProjectRepository.java @@ -0,0 +1,113 @@ +package fr.epita.assistants.ping.data.repository; + +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class ProjectRepository implements PanacheRepository { + @ConfigProperty(name = "PROJECT_DEFAULT_PATH") + String defaultPath; + + @Inject + Logger logger; + + // MODIFIENT LA DB + @Transactional + public ProjectModel createProject(UserModel owner, String name) { + ProjectModel project = new ProjectModel().withName(name).withOwner(owner); + List members = new ArrayList<>(); + members.add(owner); + project.setMembers(members); + persist(project); + defaultPath = defaultPath.charAt(defaultPath.length() - 1) == '/' ? defaultPath : defaultPath + "/"; + project.setPath(defaultPath + project.getId().toString()); + persist(project); + logger.info("Projet " + project.getId() + " créé."); + return project; + } + + // UPDATE + // On admet que le projet existe -> utilisez getProject ! + @Transactional + public void updateName(UUID id, String name) { + update("name = ?1 where id = ?2", id, name); + logger.info("Projet " + id + " mis à jour (nouveau nom : " + name + ")."); + } + + @Transactional + public void updateOwner(UUID id, UserModel newOwner) { + update("owner = ?1 where id = ?2", id, newOwner); + logger.info("Projet " + id + " mis à jour (nouveau propriétaire : " + newOwner + ")."); + } + + @Transactional + public void addMember(UUID id, UserModel member) { + ProjectModel p = find("where id = ?1", id).firstResult(); + p.getMembers().add(member); + persist(p); + logger.info("Nouveau membre (utilisateur " + member.getDisplayName() + ") " + " ajouté au projet " + id + "."); + } + + @Transactional + public void removeMember(UUID id, UserModel member) { + ProjectModel p = find("where id = ?1", id).firstResult(); + p.getMembers().remove(member); + persist(p); + logger.info("Membre (utilisateur " + member + ") " + " retiré du projet " + id + "."); + } + + // DELETE + // Admis : + // - Le projet existe -> Utilisez getProject + public void deleteProject(UUID id) { + Optional optPm = getProject(id); + if(optPm.isEmpty()) return; + ProjectModel pm = optPm.get(); + // Les membres connaissent plus ce projet + pm.getMembers().forEach(m -> m.getProjectMemberships().remove(pm)); + // Le projet connait plus ses membres + pm.getMembers().clear(); + persist(pm); + // Le projet part dans le ventre de bombardiro crocodiro + delete("where id = ?1",id); + logger.info("Projet " + id + "supprimé."); + } + + // NE MODIFIENT PAS LA DB + @Transactional + public List getAllProjects() { + List res = findAll().stream().toList(); + logger.info("Récupération de tous les projets depuis la base de données `projects`."); + for (ProjectModel elem : res) + { + logger.info("Found project: " + elem.toString()); + } + return res; + } + + @Transactional + public List getAllOwnedProjects(UserModel owner) { + var res = find("where owner.id = ?1", owner.getId()).stream().toList(); + logger.info("Récupération de tous les projets de " + owner.getDisplayName() + " depuis la base de données `projects`."); + return res; + } + + @Transactional + public Optional getProject(UUID id) { + var res = find("where id = ?1", id).firstResultOptional(); + logger.info("Récupération du projet " + id + " depuis la base de données `projects`."); + return res; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/TransactionRepository.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/TransactionRepository.java new file mode 100644 index 0000000..cc0193f --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/TransactionRepository.java @@ -0,0 +1,32 @@ +package fr.epita.assistants.ping.data.repository; + +import fr.epita.assistants.ping.data.model.TransactionModel; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class TransactionRepository implements PanacheRepository { + + public TransactionModel findById(final UUID identifier) { + return TransactionModel.findById(identifier); + } + + public Optional findByIdOptional(final UUID identifier) { + return find("where id = ?1", identifier).firstResultOptional(); + } + + public boolean deleteById(final UUID identifier) { + final Optional transaction = findByIdOptional(identifier); + + if (transaction.isEmpty()) { + return false; + } + + transaction.get().delete(); + + return true; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/UserRepository.java b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/UserRepository.java new file mode 100644 index 0000000..599a8fc --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/data/repository/UserRepository.java @@ -0,0 +1,120 @@ +package fr.epita.assistants.ping.data.repository; + +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +@ApplicationScoped +public class UserRepository implements PanacheRepository { + @Inject + Logger logger; + // @Transactional être la voie + + // MODIFIENT LA DB + @Transactional + public UserModel createUser(String name, Boolean admin, String avatar, String login, String password) { + UserModel user = new UserModel() + .withAvatar(avatar) + .withLogin(login) + .withPassword(password) + .withIsAdmin(admin) + .withDisplayName(name); + + persist(user); + logger.info("Utilisateur " + user.getId() + " créé."); + return user; + } + + // Projets dans lesquels l'utilisateur est + // id = user + // ProjectModel = le projet + @Transactional + public void joinProject(UUID id, ProjectModel project) { + logger.info("Adding user (" + id + ") to project" + project.getName()); + UserModel user = find("id = ?1", id).firstResult(); + if (user != null) { + user.getProjectMemberships().add(project); + persist(user); + } + } + + @Transactional + public void leaveProject(UUID id, ProjectModel project) { + logger.info("Removing user (" + id + ") from project" + project.getName()); + UserModel user = find("id = ?1", id).firstResult(); + if (user != null) { + user.getProjectMemberships().remove(project); + persist(user); + } + } + + // UPDATE + // On admet que le user existe -> Utilisez getUser ! + + @Transactional + public void updatePassword(UUID id, String password) { + update("password = ?1 where id = ?2", password, id); + logger.info("Utilisateur " + id + " mis à jour (nouveau mot de passe : " + password + ")."); + } + @Transactional + public void updateDisplayName(UUID id, String name) { + update("displayName = ?1 where id = ?2", name, id); + logger.info("Utilisateur " + id + " mis à jour (nouveau nom : " + name + ")."); + } + @Transactional + public void updateAvatar(UUID id, String avatar) { + update("avatar = ?1 where id = ?2", avatar, id); + logger.info("Utilisateur " + id + " mis à jour (nouvel avatar : " + avatar + ")."); + } + @Transactional + public void updateLogin(UUID id, String login) { + update("login = ?1 where id = ?2", login, id); + logger.info("Utilisateur " + id + " mis à jour (nouveau login : " + login + ")."); + } + @Transactional + public void updateIsAdmin(UUID id, Boolean admin) { + update("isAdmin = ?1 where id = ?2", admin, id); + logger.info("Utilisateur " + id + " mis à jour (nouveau statut administrateur : " + admin + ")."); + } + + // DELETE + // Admis : + // - Le user existe -> Utilisez getUser + // - Le user n'est propriétaire d'aucun projet -> Utilisez ProjectRepository + @Transactional + public void deleteUser(UUID id) { + delete("id = ?1", id); + logger.info("Utilisateur " + id + "supprimé."); + } + + // NE MODIFIENT PAS LA DB + + @Transactional + public List getAllUsers() { + var res = findAll().stream().toList(); + logger.info("Récupération de tous les utilisateurs depuis la base de données `users`."); + return res; + } + + // Getters renvoie Optional si jamais le user n'existe pas + @Transactional + public Optional getUser(UUID id) { + var res = find("where id = ?1", id).firstResultOptional(); + logger.info("Récupération de l'utilisateur (" + id + " " + ((res.isEmpty())? "[NOT FOUND]" : res.get().getDisplayName()) + ") depuis la base de données `users`."); + return res; + } + public Optional getUser(String login) { + var res = find("where login = ?1", login).firstResultOptional(); + logger.info("Récupération de l'utilisateur (" + login + ") depuis la base de données `users`."); + return res; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionCreationEntity.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionCreationEntity.java new file mode 100644 index 0000000..2f69c03 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionCreationEntity.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.domain.entity; + +import java.time.LocalDateTime; +import java.util.Currency; + +public record TransactionCreationEntity( + + Float amount, + + Currency currency, + + String label, + + String receiver_label, + + String receiver_iban, + + LocalDateTime operation_date +) {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionUpdateEntity.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionUpdateEntity.java new file mode 100644 index 0000000..a6c0c8d --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/TransactionUpdateEntity.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.domain.entity; + +import java.time.LocalDateTime; +import java.util.Currency; + +public record TransactionUpdateEntity( + + Float amount, + + Currency currency, + + String label, + + String receiverLabel, + + String receiverIban, + + LocalDateTime operationDate +) {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserCreationEntity.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserCreationEntity.java new file mode 100644 index 0000000..7b87bc3 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserCreationEntity.java @@ -0,0 +1,7 @@ +package fr.epita.assistants.ping.domain.entity; + +public record UserCreationEntity( + String login, + String password, + Boolean isAdmin +) {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserUpdateEntity.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserUpdateEntity.java new file mode 100644 index 0000000..ae237e1 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/UserUpdateEntity.java @@ -0,0 +1,7 @@ +package fr.epita.assistants.ping.domain.entity; + +public record UserUpdateEntity ( + String password, + String displayName, + String avatar +) {} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/Feature.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/Feature.java new file mode 100644 index 0000000..cf401d7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/Feature.java @@ -0,0 +1,14 @@ +package fr.epita.assistants.ping.domain.entity.feature; + +import fr.epita.assistants.ping.errors.InvalidFeatureException; + +public enum Feature { + GIT; + + public static Feature fromString(final String string) { + return switch (string) { + case "git" -> GIT; + default -> throw new InvalidFeatureException("Invalid feature parameter: " + string); + }; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/FeatureExecution.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/FeatureExecution.java new file mode 100644 index 0000000..c993e3b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/entity/feature/FeatureExecution.java @@ -0,0 +1,18 @@ +package fr.epita.assistants.ping.domain.entity.feature; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class FeatureExecution { + public final Feature feature; + public final String command; + public final List args; + + public FeatureExecution(final String feature, final String command, final List args) { + this(Feature.fromString(feature), command, args); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureDispatcher.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureDispatcher.java new file mode 100644 index 0000000..f01ed3e --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureDispatcher.java @@ -0,0 +1,31 @@ +package fr.epita.assistants.ping.domain.executor; + +import fr.epita.assistants.ping.domain.entity.feature.Feature; +import fr.epita.assistants.ping.domain.entity.feature.FeatureExecution; +import io.quarkus.runtime.Startup; +import jakarta.enterprise.context.ApplicationScoped; + +import java.io.File; +import java.security.InvalidParameterException; +import java.util.HashMap; +import java.util.Map; + +@ApplicationScoped +public class FeatureDispatcher { + private final Map features = new HashMap<>(); + + @Startup + public void init() { + features.put(Feature.GIT, new GitFeatureExecutor()); + } + + public void execute(final File projectRoot, final FeatureExecution executionRequest) { + if (!features.containsKey(executionRequest.getFeature())) { + throw new InvalidParameterException( + "Feature " + executionRequest.getFeature() + " currently does not have any implementation" + ); + } + + features.get(executionRequest.getFeature()).execute(projectRoot, executionRequest); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureExecutor.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureExecutor.java new file mode 100644 index 0000000..e67b075 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/FeatureExecutor.java @@ -0,0 +1,17 @@ +package fr.epita.assistants.ping.domain.executor; + +import fr.epita.assistants.ping.domain.entity.feature.FeatureExecution; + +import java.io.File; + +public interface FeatureExecutor { + /** + * unique tool name, e.g. "git" + */ + String name(); + + /** + * execute sub-command on the project root directory + */ + void execute(File projectRoot, FeatureExecution request); +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/GitFeatureExecutor.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/GitFeatureExecutor.java new file mode 100644 index 0000000..098536a --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/executor/GitFeatureExecutor.java @@ -0,0 +1,134 @@ +package fr.epita.assistants.ping.domain.executor; + +import fr.epita.assistants.ping.domain.entity.feature.FeatureExecution; +import fr.epita.assistants.ping.errors.InvalidCommandException; +import org.eclipse.jgit.api.AddCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class GitFeatureExecutor implements FeatureExecutor { + + private List validateAddArgs(final List args) { + return args; + } + + private List gitAddRecursive(final File contextFolder, final Path rootFolder, final String pattern) { + final Pattern asPattern = Pattern.compile(pattern); + final File[] files = contextFolder.listFiles(); + final List result = new ArrayList<>(); + + assert files != null; + + for (final File child : files) { + final Path relativePath = rootFolder.relativize(Path.of(child.getPath())); + + if (asPattern.matcher(relativePath.toString()).matches()) { + result.add(relativePath); + } + + if (child.isDirectory()) { + result.addAll(gitAddRecursive(child, rootFolder, pattern)); + } + } + + return result; + } + + private Repository loadRepository(final File contextRoot) { + try { + return new FileRepositoryBuilder() + .setGitDir(contextRoot.toPath().resolve(".git").toFile()) + .build(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void gitInit(final File contextRoot, final List _args) { + try { + Git.init().setDirectory(contextRoot).call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + + private void gitAdd(final File contextRoot, final List args) { + final List actualArgs = validateAddArgs(args); + + if (actualArgs.isEmpty()) { + throw new InvalidParameterException("Expected at least 1 argument but got none"); + } + + final Repository repository = loadRepository(contextRoot); + final Git git = new Git(repository); + AddCommand addCommand = git.add(); + final Path contextRootAsPath = contextRoot.toPath(); + final List filesToAdd = new ArrayList<>(); + + for (final String arg : actualArgs) { + if (arg == null) { + throw new InvalidParameterException("Git add patterns cannot include a null value"); + } + + final List correspondances = gitAddRecursive(contextRoot, contextRootAsPath, arg); + + if (correspondances.isEmpty()) { + throw new InvalidParameterException("Git add pattern " + arg + " does not match any file"); + } + + filesToAdd.addAll(correspondances); + } + + filesToAdd.forEach(path -> addCommand.addFilepattern(path.toString())); + + try { + addCommand.call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + + private void gitCommit(final File contextRoot, final List args) { + if (args.size() != 1) { + throw new InvalidParameterException("Expected 1 argument but got " + args.size()); + } + + final Repository repository = loadRepository(contextRoot); + final Git git = new Git(repository); + final String commitMessage = args.getFirst(); + + try { + git.commit().setMessage(commitMessage).call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + + @Override + public String name() { + return "Git"; + } + + @Override + public void execute(File projectRoot, FeatureExecution request) { + final String command = request.getCommand(); + final List args = request.getArgs(); + + switch (command) { + case "init" -> gitInit(projectRoot, args); + case "add" -> gitAdd(projectRoot, args); + case "commit" -> gitCommit(projectRoot, args); + default -> throw new InvalidCommandException(this, command); + } + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/FileSystemService.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/FileSystemService.java new file mode 100644 index 0000000..2ee3959 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/FileSystemService.java @@ -0,0 +1,497 @@ +package fr.epita.assistants.ping.domain.service; + +import fr.epita.assistants.ping.api.requests.MoveRequest; +import fr.epita.assistants.ping.api.requests.PathRequest; +import fr.epita.assistants.ping.api.responses.FSEntryResponse; +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.errors.ErrorsCode; +import fr.epita.assistants.ping.errors.UnreacheableCodeException; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.runtime.Startup; +import jakarta.annotation.Nullable; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@ApplicationScoped +public class FileSystemService { + + @ConfigProperty(name = "PROJECT_DEFAULT_PATH") + String defaultPath; + + private Path projectRootPath; + + @Inject + UserService userService; + + @Inject + ProjectService projectService; + + @Inject + Logger logger; + + @Startup + void init() { + projectRootPath = Paths.get(defaultPath); + logger.info("Set project files root path to " + projectRootPath); + } + + private ProjectModel loadContextProject(final UUID contextUserId, final UUID projectId) { + logger.debug("Attempting to load context project " + projectId + " as " + contextUserId); + final UserModel contextUser = userService.loadContextUser(contextUserId); + return projectService.loadContextProject(projectId, contextUser); + } + + private Path resolvePath(final ProjectModel project, @Nullable final String relativePath) { + final Path thisProjectRoot = projectRootPath.resolve(project.getId().toString()); + Path resolvedPath = thisProjectRoot; + + if (relativePath != null && !relativePath.isBlank()) { + resolvedPath = resolvedPath.resolve(relativePath); + } + + resolvedPath = resolvedPath.normalize(); + + if (!resolvedPath.startsWith(thisProjectRoot)) { + ErrorsCode.FORBIDDEN.throwException("Access denied: Path attempts to access outside project directory"); + } + + return resolvedPath; + } + + public Response listFolder(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Listing folder contents for: " + request.getRelativePath()); + + try { + Path dirPath = resolvePath(contextProject, request.getRelativePath()); + logger.info("Listing folder contents for: " + request.getRelativePath()); + + if (!Files.exists(dirPath)) { + logger.error("Folder not found: " + dirPath); + ErrorsCode.NOT_FOUND.throwException("Folder not found"); + } + + if (!Files.isDirectory(dirPath)) { + logger.error("Path is not a directory: " + dirPath); + ErrorsCode.BAD_REQUEST.throwException("Path is not a directory"); + } + + List entries = Files.list(dirPath) + .map(path -> new FSEntryResponse( + path.getFileName().toString(), + //dirPath.relativize(path).toString(), + projectRootPath.resolve(contextProject.getId().toString()).relativize(path).toString(), + Files.isDirectory(path) + )) + .collect(Collectors.toList()); + + logger.info("Listing folder contents at path: " + request.getRelativePath()); + if (entries.isEmpty()) { + logger.info("Folder is empty"); + } + + logger.info("Found " + entries.size() + " entries in folder: " + request.getRelativePath()); + return Response.ok(entries).build(); + + } catch (IOException e) { + logger.error("Failed to list folder contents: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to list folder contents: " + e.getMessage()); + } + + logger.error("Unreachable code reached in listFolder method"); + throw new UnreacheableCodeException(); + } + + public Response deleteFolder(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Deleting folder: " + request.getRelativePath()); + + try { + if (request.getRelativePath() == null) { + logger.error("Invalid path: null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null or empty"); + } + + Path dirPath = resolvePath(contextProject, request.getRelativePath()); + Path projectRoot = projectRootPath.resolve(contextProject.getId().toString()); + + if (!Files.exists(dirPath)) { + logger.error("Folder not found: " + dirPath); + ErrorsCode.NOT_FOUND.throwException("Folder not found"); + } + + if (!Files.isDirectory(dirPath)) { + logger.error("Path is not a directory: " + dirPath); + ErrorsCode.BAD_REQUEST.throwException("Path is not a directory"); + } + + logger.info("Removing folder and its contents: " + dirPath); + + if (dirPath.equals(projectRoot)) { + logger.info("Emptying root folder contents without deleting root"); + Files.walk(dirPath) + .sorted((a, b) -> b.compareTo(a)) + .filter(path -> !path.equals(dirPath)) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + logger.error("Failed to remove file: " + path + " - " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException( + "Failed to remove file " + path + ": " + e.getMessage()); + } + }); + } else { + Files.walk(dirPath) + .sorted((a, b) -> b.compareTo(a)) + .forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + logger.error("Failed to remove file: " + path + " - " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException( + "Failed to remove file " + path + ": " + e.getMessage()); + } + }); + } + + logger.info("Successfully deleted folder: " + dirPath); + return Response.noContent().build(); + + } catch (IOException e) { + logger.error("Failed to delete folder: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to delete folder: " + e.getMessage()); + } + + logger.error("Unreachable code reached in deleteFolder method"); + throw new UnreacheableCodeException(); + } + + public Response createFolder(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Creating folder: " + request.getRelativePath()); + + try { + logger.info("Attempting to create folder with path: " + request.getRelativePath()); + if (request.getRelativePath() == null) { + logger.error("Create folder failed: path is null"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null"); + } + if (request.getRelativePath().isEmpty()) { + logger.error("Create folder failed: path is empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be empty"); + } + + Path dirPath = resolvePath(contextProject, request.getRelativePath()); + + if (Files.exists(dirPath)) { + logger.error("Folder already exists: " + dirPath); + ErrorsCode.CONFLICT.throwException("Folder already exists"); + } + + logger.info("Creating new folder at path: " + request.getRelativePath()); + Files.createDirectories(dirPath); + + return Response.status(Response.Status.CREATED) + .entity(new FSEntryResponse( + dirPath.getFileName().toString(), + request.getRelativePath(), + true + )).build(); + + } catch (IOException e) { + logger.error("Failed to create folder: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to create folder: " + e.getMessage()); + } + + logger.error("Unreachable code reached in createFolder method"); + throw new UnreacheableCodeException(); + } + + public Response createFile(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Creating file: " + request.getRelativePath()); + + try { + if (request.getRelativePath() == null || request.getRelativePath().isEmpty()) { + logger.error("Invalid path: null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null or empty"); + } + + Path filePath = resolvePath(contextProject, request.getRelativePath()); + + logger.info("Creating new file at path: " + request.getRelativePath()); + if (Files.exists(filePath)) { + logger.error("Create file failed: file already exists at " + filePath); + ErrorsCode.CONFLICT.throwException("File already exists"); + } + + Files.createDirectories(filePath.getParent()); + Files.createFile(filePath); + + return Response.status(Response.Status.CREATED) + .entity(new FSEntryResponse( + filePath.getFileName().toString(), + request.getRelativePath(), + false + )).build(); + + } catch (IOException e) { + logger.error("Failed to create file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to create file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in createFile method"); + throw new UnreacheableCodeException(); + } + + public Response deleteFile(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Deleting file: " + request.getRelativePath()); + + try { + if (request.getRelativePath() == null || request.getRelativePath().isEmpty()) { + logger.error("Invalid path: null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null or empty"); + } + + Path filePath = resolvePath(contextProject, request.getRelativePath()); + Path projectRoot = projectRootPath.resolve(contextProject.getId().toString()); + + logger.info("Attempting to delete file: " + request.getRelativePath()); + if (filePath.equals(projectRoot)) { + logger.error("Cannot delete project root directory"); + ErrorsCode.FORBIDDEN.throwException("Cannot delete project root directory"); + } + + if (!Files.exists(filePath)) { + logger.error("Delete file failed: file not found at " + filePath); + ErrorsCode.NOT_FOUND.throwException("File not found"); + } + + if (Files.isDirectory(filePath)) { + logger.error("Path is a directory, not a file: " + filePath); + ErrorsCode.BAD_REQUEST.throwException("Path is a directory, not a file"); + } + + logger.info("Removing file: " + filePath); + Files.delete(filePath); + + return Response.noContent().build(); + + } catch (IOException e) { + logger.error("Failed to delete file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to delete file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in deleteFile method"); + throw new UnreacheableCodeException(); + } + + public Response readFile(final UUID contextUserId, final UUID projectId, final PathRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Reading file: " + request.getRelativePath()); + + try { + Path filePath = resolvePath(contextProject, request.getRelativePath()); + + logger.info("Downloading file from path: " + request.getRelativePath()); + if (!Files.exists(filePath)) { + logger.error("Download failed: file not found at " + filePath); + ErrorsCode.NOT_FOUND.throwException("File not found"); + } + + if (Files.isDirectory(filePath)) { + logger.error("Path is a directory, not a file: " + filePath); + ErrorsCode.BAD_REQUEST.throwException("Path is a directory, not a file"); + } + + logger.info("Reading file content: " + filePath); + byte[] content = Files.readAllBytes(filePath); + + return Response.ok(content).build(); + + } catch (IOException e) { + logger.error("Failed to read file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to read file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in readFile method"); + throw new UnreacheableCodeException(); + } + + public Response updateFile(final UUID contextUserId, final UUID projectId, final PathRequest request, + final String content) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Trying to update file " + request.getRelativePath() + " from project " + projectId); + + try { + Path filePath = resolvePath(contextProject, request.getRelativePath()); + + if (!Files.exists(filePath)) { + logger.error("File not found: " + filePath); + ErrorsCode.NOT_FOUND.throwException("File not found"); + } + + if (Files.isDirectory(filePath)) { + logger.error("Path is a directory, not a file: " + filePath); + ErrorsCode.BAD_REQUEST.throwException("Path is a directory, not a file"); + } + + logger.info("Updating file content: " + filePath); + Files.writeString(filePath, content); + + return Response.ok(new FSEntryResponse( + filePath.getFileName().toString(), + request.getRelativePath(), + false + )).build(); + + } catch (IOException e) { + logger.error("Failed to update file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to update file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in updateFile method"); + throw new UnreacheableCodeException(); + } + + public Response moveFile(final UUID contextUserId, final UUID projectId, final MoveRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Moving file from " + request.getSrc() + " to " + request.getDst() + " in project " + projectId); + + try { + if (request.getSrc() == null || request.getSrc().isEmpty() || + request.getDst() == null || request.getDst().isEmpty()) { + logger.error("Invalid path: source or destination is null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Source and destination paths cannot be null or empty"); + } + + Path sourcePath = resolvePath(contextProject, request.getSrc()); + Path targetPath = resolvePath(contextProject, request.getDst()); + + logger.info("Moving file from " + request.getSrc() + " to " + request.getDst()); + if (!Files.exists(sourcePath)) { + logger.error("Move file failed: source file not found at " + sourcePath); + ErrorsCode.NOT_FOUND.throwException("Source file not found"); + } + + if (Files.isDirectory(sourcePath)) { + logger.error("Source path is a directory, not a file: " + sourcePath); + ErrorsCode.BAD_REQUEST.throwException("Source path is a directory, not a file"); + } + + if (Files.exists(targetPath)) { + logger.error("Target file already exists: " + targetPath); + ErrorsCode.CONFLICT.throwException("Target file already exists"); + } + + Files.createDirectories(targetPath.getParent()); + Files.move(sourcePath, targetPath); + + return Response.noContent().build(); + + } catch (IOException e) { + logger.error("Failed to move file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to move file: " + e.getMessage()); + } + + logger.error("Unreachable code reached in moveFile method"); + throw new UnreacheableCodeException(); + } + + public Response moveFolder(final UUID contextUserId, final UUID projectId, final MoveRequest request) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Moving folder from " + request.getSrc() + " to " + request.getDst() + " in project " + projectId); + + try { + if (request.getSrc() == null || request.getSrc().isEmpty() || + request.getDst() == null || request.getDst().isEmpty()) { + logger.error("Invalid path: source or destination is null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Source and destination paths cannot be null or empty"); + } + + Path sourcePath = resolvePath(contextProject, request.getSrc()); + Path targetPath = resolvePath(contextProject, request.getDst()); + + logger.info("Moving folder from " + request.getSrc() + " to " + request.getDst()); + if (!Files.exists(sourcePath)) { + logger.error("Source folder not found: " + sourcePath); + ErrorsCode.NOT_FOUND.throwException("Source folder not found"); + } + + if (!Files.isDirectory(sourcePath)) { + logger.error("Source path is not a directory: " + sourcePath); + ErrorsCode.BAD_REQUEST.throwException("Source path is not a directory"); + } + + if (Files.exists(targetPath)) { + logger.error("Target folder already exists: " + targetPath); + ErrorsCode.CONFLICT.throwException("Target folder already exists"); + } + + Files.createDirectories(targetPath.getParent()); + Files.move(sourcePath, targetPath); + + return Response.noContent().build(); + + } catch (IOException e) { + logger.error("Failed to move folder: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to move folder: " + e.getMessage()); + } + + logger.error("Unreachable code reached in moveFolder method"); + throw new UnreacheableCodeException(); + } + + public Response uploadFile(final UUID contextUserId, final UUID projectId, String path, InputStream fileContent) { + ProjectModel contextProject = loadContextProject(contextUserId, projectId); + logger.info("Uploading file to " + projectId + "/" + path); + + try { + logger.info("Uploading file to path: " + path); + if (path == null || path.isEmpty()) { + logger.error("Upload failed: path is null or empty"); + ErrorsCode.BAD_REQUEST.throwException("Path cannot be null or empty"); + } + + Path filePath = resolvePath(contextProject, path); + + Files.createDirectories(filePath.getParent()); + + Files.copy(fileContent, filePath, StandardCopyOption.REPLACE_EXISTING); + + return Response.status(Response.Status.CREATED) + .entity(new FSEntryResponse( + filePath.getFileName().toString(), + path, + false + )).build(); + + } catch (IOException e) { + logger.error("Failed to upload file: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to upload file: " + e.getMessage()); + } finally { + logger.info("Closing file content stream"); + try { + fileContent.close(); + } catch (IOException e) { + logger.error("Error closing file content stream: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Error happened while closing file: " + e.getMessage()); + } + } + + logger.error("Unreachable code reached in uploadFile method"); + throw new UnreacheableCodeException(); + } +} \ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/ProjectService.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/ProjectService.java new file mode 100644 index 0000000..b54ebfd --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/ProjectService.java @@ -0,0 +1,451 @@ +package fr.epita.assistants.ping.domain.service; + +import fr.epita.assistants.ping.api.responses.ProjectResponse; +import fr.epita.assistants.ping.converters.response.ProjectResponseConverter; +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.ProjectRepository; +import fr.epita.assistants.ping.data.repository.UserRepository; +import fr.epita.assistants.ping.domain.entity.feature.FeatureExecution; +import fr.epita.assistants.ping.domain.executor.FeatureDispatcher; +import fr.epita.assistants.ping.errors.ErrorsCode; +import fr.epita.assistants.ping.errors.InvalidFeatureException; +import fr.epita.assistants.ping.utils.Logger; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.InvalidParameterException; +import java.util.*; + +@ApplicationScoped +public class ProjectService { + + @Inject + FeatureDispatcher featureDispatcher; + + @Inject + ProjectRepository projectRepository; + + @Inject + UserRepository userRepository; + + @Inject + UserService userService; + + @Inject + ProjectResponseConverter projectResponseConverter; + + @ConfigProperty(name = "PROJECT_DEFAULT_PATH", defaultValue = "/var/www/projects/") + String PROJECT_DEFAULT_PATH; + + @Inject + Logger logger; + + private boolean isIgnorableString(final String string) { + return string == null || string.isBlank(); + } + + public ProjectModel loadContextProject(final UUID contextProjectId, final UserModel contextUser) { + Optional contextProjectQuery = projectRepository.getProject(contextProjectId); + + if (contextProjectQuery.isEmpty()) { + logger.error("Tried to process query with an invalid project id : " + contextProjectId); + ErrorsCode.NOT_FOUND.throwException("Invalid project id"); + } + + final ProjectModel contextProject = contextProjectQuery.get(); + + if (!contextUser.getIsAdmin() && !contextProject.hasMember(contextUser)) { + logger.error( + "Tried to process query implying project " + contextProjectId + " with an user who is not " + + "part of said project: " + contextUser.getId() + ); + ErrorsCode.FORBIDDEN.throwException("User is not allowed to access project " + contextProjectId); + } + + return contextProject; + } + + /** + * List projects of current user + * + * @param onlyOwned show only owned projects (not member ones) + * @return 200 - A list of projects + */ + @Transactional + public List listProjectsOfUser(final UUID contextUuid, Boolean onlyOwned) { + final UserModel contextUser = userService.loadContextUser(contextUuid); + + boolean onlyowned_ = onlyOwned != null ? onlyOwned : false; + logger.info("Asking listProjectsOfUser : onlyOwned? " + onlyowned_); + + final List projects = onlyowned_ + ? projectRepository.getAllOwnedProjects(contextUser) + : new ArrayList<>(contextUser.getProjectMemberships()); + + return projectResponseConverter.convert(projects); + } + + /** + * Create a new project with a name + * + * @param projectName + * @return 200 - The created project + * @throws RuntimeException 400 - Project name is invalid + */ + @Transactional + public ProjectResponse createProject(final UUID contextUuid, String projectName) { + logger.info("Asking createProject"); + + final UserModel contextUser = userService.loadContextUser(contextUuid); + + if (projectName == null || projectName.isEmpty()) { + logger.error("Project name null or empty"); + ErrorsCode.BAD_REQUEST.throwException("The project name is invalid (null or empty for example)"); + } + + ProjectModel pm = projectRepository.createProject(contextUser,projectName); + logger.log("New project " + projectName + " created"); + + userRepository.joinProject(contextUuid, pm); + + try { + Files.createDirectories(Path.of(PROJECT_DEFAULT_PATH,pm.getId().toString())); + } catch (IOException e) { + logger.error("Le gros paf de projet ne peut être construit : "+Path.of(PROJECT_DEFAULT_PATH,pm.getId().toString())); + } + + return projectResponseConverter.convert(pm); + } + + /** + * List all projects + * + * @return 200 - list of projects + * @throws 403 - the user is not an admin + */ + @Transactional + public List listAllProjects() { + logger.info("Asking listAllProjects"); + return projectResponseConverter.convert(projectRepository.getAllProjects()); + } + + /** + * Update a project, changing at least name or owner + * + * @param projectId the current project id + * @param newProjectName the nullable new project name + * @param newProjectOwner the nullable new project owner + * @return 200 - the updated project + * @throws RuntimeException 400 - The name and owner are both null + * @throws RuntimeException 404 - Project with ID not found + */ + @Transactional + public ProjectResponse updateProject(final UUID contextUuid, String projectId, String newProjectName, String newProjectOwner) { + logger.info("Asking updateProject"); + final UserModel contextUser = userService.loadContextUser(contextUuid); + + if (newProjectName == null && newProjectOwner == null) { + logger.error("Both the name and the new owner are null"); + ErrorsCode.BAD_REQUEST.throwException("Both the name and the new owner are null"); + } + + Optional optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project not found (Bad UUID), while updating project"); + ErrorsCode.NOT_FOUND.throwException("Project not found (Bad UUID), while updating project"); + } + if (optProj.isEmpty()) { + logger.error("Project not found, while updating project"); + ErrorsCode.NOT_FOUND.throwException("Project not found, while updating project"); + } + var proj = optProj.get(); + + if (!contextUser.getIsAdmin() && !proj.getOwner().getId().toString().equals(contextUuid.toString())) { + logger.error("The user isn't admin nor owner"); + ErrorsCode.FORBIDDEN.throwException("The user isn't admin nor owner. (owner:" + proj.getOwner().getId() + ",user:" + contextUuid + ")"); + } + if (newProjectOwner != null && !newProjectOwner.equals(proj.getOwner().toString())) { + Optional optNewOwner = Optional.empty(); + try { + optNewOwner = userRepository.getUser(UUID.fromString(newProjectOwner)); + } catch (Exception e) { + logger.error("New owner not found (Bad UUID), while updating project"); + ErrorsCode.NOT_FOUND.throwException("New owner not found (Bad UUID), while updating project"); + } + + if(optNewOwner.isEmpty()) + { + logger.error("New owner not found, while updating project"); + ErrorsCode.NOT_FOUND.throwException("New owner not found, while updating project"); + } + + optNewOwner.ifPresent(u -> { + if(proj.getMembers().stream().noneMatch(pm -> u.getId().equals(pm.getId()))) + { + logger.error("The new owner isn't member of project, while updating project"); + ErrorsCode.NOT_FOUND.throwException("The new owner isn't member of project, while updating project"); + } + proj.setOwner(u); + }); + } + if (newProjectName != null) + proj.setName(newProjectName); + + projectRepository.persist(proj); + logger.info("Updating project !"); + return projectResponseConverter.convert(proj); + } + + /** + * Get a project + * + * @param contextUuid + * @param projectId UUID of the project + * @return 200 - The project + * @throws RuntimeException 404 - Project not found + */ + @Transactional + public ProjectResponse getProject(UUID contextUuid, String projectId) { + logger.info("Aking getProject"); + Optional optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project " + projectId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found (not an UUID)"); + } + if (optProj.isEmpty()) { + logger.error("Project " + projectId + " not found"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found"); + } + var proj = optProj.get(); + + final UserModel contextUser = userService.loadContextUser(contextUuid); + if (!contextUser.getIsAdmin() && proj.getMembers().stream().noneMatch(m -> m.getId() == contextUser.getId())) { + logger.error("The user is not allowed to access the project"); + ErrorsCode.FORBIDDEN.throwException("The user is not allowed to access the project"); + } + logger.info("Got project !"); + return projectResponseConverter.convert(proj); + } + + /** + * Deletes a projet, if the user is owner or admin + * + * @param contextUuid + * @param projectId the UUID of the project + * @throws RuntimeException 404 - Project not found + */ + @Transactional + public void deleteProject(UUID contextUuid, String projectId) { + logger.info("Asking deleteProject"); + Optional optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project " + projectId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found (not an UUID)"); + } + if (optProj.isEmpty()) { + logger.error("Project " + projectId + " not found"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found"); + } + var proj = optProj.get(); + + final UserModel contextUser = userService.loadContextUser(contextUuid); + if (!contextUser.getIsAdmin() && !proj.getOwner().getId().toString().equals(contextUuid.toString())) { + logger.error("The user isn't admin nor owner"); + ErrorsCode.FORBIDDEN.throwException("The user isn't admin nor owner. (owner:" + proj.getOwner().getId() + ",user:" + contextUuid + ")"); + } + + try { + Files.delete(Path.of(PROJECT_DEFAULT_PATH,proj.getId().toString())); + } catch (IOException e) { + logger.error( + "Le gros paf de projet ne peut être détruit : " + + Path.of(PROJECT_DEFAULT_PATH,proj.getId().toString()) + + ", raison: " + e.getMessage() + ); + ErrorsCode.INTERNAL_ERROR.throwException( + "Le gros paf de projet " + projectId + "ne peut être détruit. Raison : " + + e.getMessage() + ); + } + + projectRepository.deleteProject(proj.getId()); + logger.error("Kaboom deleted project"); + } + + /** + * Add a user as a project member. + * + * @param contextUuid + * @param projectId the UUID of project + * @param userId the UUID of user + * @throws RuntimeException 404 - User or project not found + * @throws RuntimeException 400 - userId is empty or null + * @throws RuntimeException 409 - The user is already a member + */ + @Transactional + public void addMember(UUID contextUuid, String projectId, String userId) { + logger.info("Asking addMember"); + Optional optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project " + projectId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found (not an UUID)"); + } + + if (optProj.isEmpty()) { + logger.error("Project " + projectId + " not found"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found"); + } + ProjectModel proj = optProj.get(); + + if (userId == null || userId.isEmpty()) { + logger.error("The userId is invalid (null or empty for example)"); + ErrorsCode.BAD_REQUEST.throwException("The userId is invalid (null or empty for example)"); + } + + Optional optUser = Optional.empty(); + try { + optUser = userRepository.getUser(UUID.fromString(userId)); + } catch (Exception e) { + logger.error("User " + userId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("User " + userId + " not found (not an UUID)"); + } + + if (optUser.isEmpty()) { + logger.error("User " + userId + " not found"); + ErrorsCode.NOT_FOUND.throwException("User " + userId + " not found"); + } + var user = optUser.get(); + + if (proj.getMembers().stream().map(UserModel::getLogin).anyMatch(login -> login.equals(user.getLogin()))) { + logger.error("The user is already a member of the project"); + ErrorsCode.CONFLICT.throwException("The user is already a member of the project"); + } + final UserModel contextUser = userService.loadContextUser(contextUuid); + if (!contextUser.getIsAdmin() && proj.getMembers().stream().noneMatch(m -> m.getId() == contextUser.getId())) { + logger.error("The user is not allowed to access the project"); + ErrorsCode.FORBIDDEN.throwException("The user is not allowed to access the project"); + } + projectRepository.addMember(proj.getId(),user); + logger.info("Welcome to the crew member !"); + } + + /** + * Execute a feature on the project. + * + * @param contextUuid + * @param projectId project UUID + * @param feature + * @param command + * @param params + * @throws RuntimeException 400 - feature is null or command is null + * @throws RuntimeException 404 - Project not found + */ + public void execute(final UUID contextUuid, final UUID projectId, String feature, String command, List params) { + final UserModel contextUser = userService.loadContextUser(contextUuid); + final ProjectModel contextProject = loadContextProject(projectId, contextUser); + + if (isIgnorableString(feature)) { + logger.error("Attempted command execution with an empty feature"); + ErrorsCode.BAD_REQUEST.throwException("Attempted command execution with an empty feature"); + } + + if (isIgnorableString(command)) { + logger.error("Attempted command execution with an empty command"); + ErrorsCode.BAD_REQUEST.throwException("Attempted command execution with an empty command"); + } + + logger.info( + "Attempting command using feature " + feature + " on project " + projectId + ": " + command + + " with params " + String.join(" ", params) + ); + + try { + File commandSource = Path.of(contextProject.getPath()).toFile(); + FeatureExecution commandInstructions = new FeatureExecution(feature, command, params); + + featureDispatcher.execute(commandSource, commandInstructions); + } catch (InvalidFeatureException e) { + logger.error("Caught an InvalidFeatureException: " + e.getMessage()); + ErrorsCode.NOT_FOUND.throwException("Feature " + feature + " does not lead to any existing feature"); + } catch (InvalidParameterException e) { + logger.error("Caught an InvalidParameterException: " + e.getMessage()); + ErrorsCode.BAD_REQUEST.throwException("Invalid argument: " + e.getMessage()); + } catch (RuntimeException e) { + logger.error("Caught a RuntimeException: " + e.getMessage()); + ErrorsCode.INTERNAL_ERROR.throwException("Failed to run command: " + e.getMessage()); + } + } + + @Transactional + public void removeMember(UUID contextUuid, String projectId, String userId) { + logger.info("Asking removeMember"); + Optional optProj = Optional.empty(); + try { + optProj = projectRepository.getProject(UUID.fromString(projectId)); + } catch (Exception e) { + logger.error("Project " + projectId + " not found (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found (not an UUID)"); + } + + if (optProj.isEmpty()) { + logger.error("Project " + projectId + " not found"); + ErrorsCode.NOT_FOUND.throwException("Project " + projectId + " not found"); + } + ProjectModel proj = optProj.get(); + + if (userId == null || userId.isEmpty()) { + logger.error("The userId is invalid (null or empty for example)"); + ErrorsCode.BAD_REQUEST.throwException("The userId is invalid (null or empty for example)"); + } + final UserModel contextUser = userService.loadContextUser(contextUuid); + if (!contextUser.getIsAdmin() && !proj.getOwner().getId().toString().equals(contextUuid.toString())) { + logger.error("The user isn't admin nor owner"); + ErrorsCode.FORBIDDEN.throwException("The user isn't admin nor owner. (owner:" + proj.getOwner().getId() + ",user:" + contextUuid + ")"); + } + + Optional optUser = Optional.empty(); + try { + optUser = userRepository.getUser(UUID.fromString(userId)); + } catch (Exception e) { + logger.error("The userId is invalid (not an UUID)"); + ErrorsCode.NOT_FOUND.throwException("The userId is invalid (not an UUID)"); + } + + if (optUser.isEmpty()) { + logger.error("The userId is invalid"); + ErrorsCode.NOT_FOUND.throwException("The userId is invalid"); + } + var user = optUser.get(); + + if (proj.getMembers().stream().map(UserModel::getLogin).noneMatch(login -> login.equals(user.getLogin()))) { + logger.error("The user is not a member of the project"); + ErrorsCode.FORBIDDEN.throwException("The user is not a member of the project"); + } + + if(proj.getOwner().getId().toString().equals(user.getId().toString())) + { + logger.error("An owner can't remove himself from his project"); + ErrorsCode.FORBIDDEN.throwException("An owner can't remove himself from his project"); + } + + userRepository.leaveProject(user.getId(),proj); + projectRepository.removeMember(proj.getId(),user); + + logger.info("Farewell, bro"); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/TransactionService.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/TransactionService.java new file mode 100644 index 0000000..69192cb --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/TransactionService.java @@ -0,0 +1,119 @@ +package fr.epita.assistants.ping.domain.service; + +import fr.epita.assistants.ping.data.model.TransactionModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.TransactionRepository; +import fr.epita.assistants.ping.domain.entity.TransactionCreationEntity; +import fr.epita.assistants.ping.domain.entity.TransactionUpdateEntity; +import fr.epita.assistants.ping.errors.ErrorsCode; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class TransactionService { + + @Inject + UserService userService; + + @Inject + TransactionRepository transactionRepository; + + private TransactionModel retrieveTransaction(final UUID transactionId) { + final Optional transaction = transactionRepository.findByIdOptional(transactionId); + + if (transaction.isEmpty()) { + ErrorsCode.NOT_FOUND.throwException("transaction does not exist"); + } + + return transaction.get(); + } + + private float computeEcoScore(final TransactionModel transaction) { + /** + * On considere qu'une transaction peut consommer aux alentours de 2g + * de CO² enempreinte carbone. + * On considere aussi que la somme de la transaction a aussi un impact + * minime. + * Sources : daf-mag.fr, delubac.com + */ + return 1.0F + (float) (Math.random() * 2) + transaction.getAmount() * 0.000005F; + } + + public Float getAverageEcoScore(final UUID contextUserId) { + return (float) (getAllTransactions(contextUserId) + .stream() + .mapToDouble(transaction -> transaction.getEcoScore().doubleValue()) + .average() + .orElse(0)); + } + + public List getAllTransactions(final UUID contextUserId) { + userService.loadContextUser(contextUserId); + return transactionRepository.listAll(); + } + + public TransactionModel getTransaction(final UUID contextUserId, final UUID transactionId) { + userService.loadContextUser(contextUserId); + return retrieveTransaction(transactionId); + } + + @Transactional + public TransactionModel createTransaction(final UUID contextUserId, final TransactionCreationEntity request) { + final UserModel contextUser = userService.loadContextUser(contextUserId); + + final TransactionModel newTransaction = new TransactionModel() + .withLabel(request.label()) + .withAmount(request.amount()) + .withCurrency(request.currency()) + .withReceiverLabel(request.receiver_label()) + .withReceiverIban(request.receiver_iban()) + .withCreationUser(contextUser); + + final float transactionEcoScore = computeEcoScore(newTransaction); + + newTransaction.setEcoScore(transactionEcoScore); + + newTransaction.persist(); + + return newTransaction; + } + + @Transactional + public TransactionModel updateTransaction(final UUID contextUserId, final UUID updatedTransactionId, + final TransactionUpdateEntity request) { + userService.loadContextUser(contextUserId); + final TransactionModel transaction = retrieveTransaction(updatedTransactionId); + final float transactionEcoScore = computeEcoScore(transaction); + + transaction.setAmount(request.amount()); + transaction.setLabel(request.label()); + transaction.setCurrency(request.currency()); + transaction.setReceiverLabel(request.receiverLabel()); + transaction.setReceiverIban(request.receiverIban()); + transaction.setOperationDate(request.operationDate()); + + transaction.setEcoScore(transactionEcoScore); + transaction.setUpdateTimestamp(LocalDateTime.now()); + + transaction.persist(); + + return transaction; + } + + @Transactional + public TransactionModel deleteTransaction(final UUID contextUserId, final UUID deletedTransactionId) { + userService.loadContextUser(contextUserId); + + final TransactionModel transaction = retrieveTransaction(deletedTransactionId); + + transactionRepository.deleteById(deletedTransactionId); + + return transaction; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/UserService.java b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/UserService.java new file mode 100644 index 0000000..42be213 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/domain/service/UserService.java @@ -0,0 +1,280 @@ +package fr.epita.assistants.ping.domain.service; + +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.ProjectRepository; +import fr.epita.assistants.ping.data.repository.UserRepository; +import fr.epita.assistants.ping.domain.entity.UserCreationEntity; +import fr.epita.assistants.ping.domain.entity.UserUpdateEntity; +import fr.epita.assistants.ping.errors.ErrorsCode; +import fr.epita.assistants.ping.utils.Logger; +import fr.epita.assistants.ping.utils.TokenFactory; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.WebApplicationException; +import org.apache.maven.surefire.shared.lang3.StringUtils; + +import java.util.*; + + +@ApplicationScoped +public class UserService { + + private class UserCreationValidator { + + private void validateUserLoginValid(final String login) throws WebApplicationException { + if (login == null) { + logger.error("Attempted to create a new user with an empty login"); + ErrorsCode.BAD_REQUEST.throwException("Username should not be empty or null"); + } + + int totalDots = StringUtils.countMatches(login, '.'); + int totalDashes = StringUtils.countMatches(login, '_'); + + if (totalDots + totalDashes != 1) { + logger.error("Attempted to create a new user with an invalid login: " + login); + ErrorsCode.BAD_REQUEST.throwException("Login must only have at most a dot or a dash"); + } + } + + private void validateUserLoginExistence(final String login) throws WebApplicationException { + if (userRepository.getUser(login).isPresent()) { + logger.error("Attempted to create a new user with an already existing login: " + login); + ErrorsCode.CONFLICT.throwException("User with login " + login + " already exists"); + } + } + + private void validateUserPasswordValid(final String password) throws WebApplicationException { + if (password == null) { + logger.error("Attempted to create a new user with an empty password"); + ErrorsCode.BAD_REQUEST.throwException("Password should not be empty or null"); + } + } + + public void validate(final UserCreationEntity request) throws WebApplicationException { + validateUserLoginValid(request.login()); + validateUserPasswordValid(request.password()); + validateUserLoginExistence(request.login()); + } + } + + UserCreationValidator userCreationValidator = new UserCreationValidator(); + + @Inject + Logger logger; + + @Inject + TokenFactory tokenFactory; + + @Inject + UserRepository userRepository; + + @Inject + ProjectRepository projectRepository; + + private boolean isIgnorableString(final String string) { + return string == null || string.isBlank(); + } + + private String generateDefaultName(final String login) { + final String clean_login = login + .replace('.', ' ') + .replace('_', ' '); + + if (Objects.equals(clean_login, login)) { + return login; + } + + final StringBuilder builder = new StringBuilder(); + + for (String word : clean_login.split(" ")) { + builder.append(StringUtils.capitalize(word)); + builder.append(' '); + } + + final String name = builder.toString(); + + return name.substring(0, name.length() - 1); + } + + public UserModel loadContextUser(final UUID userId) throws WebApplicationException { + final Optional user = userRepository.getUser(userId); + + if (user.isEmpty()) { + logger.error("Usage of JWT carrying an invalid user detected. UserId : " + userId); + + ErrorsCode.UNAUTHORIZED.throwException( + "Session JWT lead to an user who does not exist. You may need to login again." + ); + } + + return user.get(); + } + + public String login(final String username, final String password) throws WebApplicationException { + if (isIgnorableString(username)) { + logger.error("Connection attempted with an empty username"); + ErrorsCode.BAD_REQUEST.throwException("Username field is empty or null"); + } + if (isIgnorableString(password)) { + logger.error("Connection attempted with an empty password"); + ErrorsCode.BAD_REQUEST.throwException("Password field is empty or null"); + } + + logger.debug("Attempting a login as " + username); + + final Optional user_query = userRepository.getUser(username); + + if (user_query.isEmpty()) { + logger.error("Connection attempted with a non-existant user: " + username); + + ErrorsCode.UNAUTHORIZED.throwException("User does not exist"); + } + + final UserModel user = user_query.get(); + + if (!Objects.equals(user.getPassword(), password)) { + logger.error("Failed connection for username " + username); + + ErrorsCode.UNAUTHORIZED.throwException("Password is incorrect"); + } + + logger.debug("Logging " + " succeeded as " + user); + + String token = tokenFactory.generateLoginToken(user); + + logger.info("Created token " + token); + + return token; + } + + public String refreshToken(final UUID contextUserUuid) { + final UserModel contextUser = loadContextUser(contextUserUuid); + + final String token = tokenFactory.generateLoginToken(contextUser); + + logger.info("Refreshed token for user " + contextUserUuid + " with a new one: " + token); + + return token; + } + + public List getAllUsers() { + List users = userRepository.getAllUsers(); + + logger.debug("GetAllUsers query returned the following list : [\n"); + + users.forEach((user) -> logger.debug(user + ",\n")); + + logger.debug("]"); + + return users; + } + + public UserModel getUser(final UUID contextUserId, final UUID searchedUserId) throws WebApplicationException { + logger.debug("Attempting to retrieve user " + searchedUserId + " as " + contextUserId); + + final UserModel contextUser = loadContextUser(contextUserId); + final Optional userSearch = userRepository.getUser(searchedUserId); + + if (userSearch.isEmpty()) { + logger.error("Query to user " + searchedUserId + " leaded to a failing outcome"); + ErrorsCode.NOT_FOUND.throwException("User does not exist"); + } + + final UserModel user = userSearch.get(); + final boolean canAccess = contextUser.getIsAdmin() + || contextUser.getId().compareTo(user.getId()) == 0; + + if (!canAccess) { + logger.error( + "Query about user " + searchedUserId + " failed because user " + contextUserId + + " is not allowed to read their information." + ); + + ErrorsCode.FORBIDDEN.throwException( + "Only admins can read information from different users than the current one" + ); + } + + logger.debug("Returning information about user : " + user); + + return user; + } + + @Transactional + public UserModel createUser(final UUID contextUserId, final UserCreationEntity request) + throws WebApplicationException { + userCreationValidator.validate(request); + + final String defaultName = generateDefaultName(request.login()); + + final UserModel new_user = userRepository.createUser( + defaultName, + request.isAdmin(), + null, + request.login(), + request.password() + ); + + logger.info( + "Successfully created user as " + contextUserId + " with parameters " + + "id : " + new_user.getId() + ", " + + "login: " + request.login() + ", " + + "defaultName: " + defaultName + ", " + + "isAdmin: " + request.isAdmin() + ); + + return new_user; + } + + @Transactional + public UserModel updateUser(final UUID contextUserId, + final UUID subjectUserId, + final UserUpdateEntity updateUserRequest) throws WebApplicationException { + final UserModel safe_user = getUser(contextUserId, subjectUserId); + final UUID safe_id = safe_user.getId(); + + logger.info( + "Updating new user with the following information : " + + "avatar: " + updateUserRequest.avatar() + ", " + + "displayName: " + updateUserRequest.displayName() + ", " + + "is password changed ? " + isIgnorableString(updateUserRequest.avatar()) + ); + + final String newAvatar = isIgnorableString(updateUserRequest.avatar()) + ? null + : updateUserRequest.avatar(); + + userRepository.updateAvatar(safe_id, newAvatar); + safe_user.setAvatar(newAvatar); + + if (!isIgnorableString(updateUserRequest.displayName())) { + userRepository.updateDisplayName(safe_id, updateUserRequest.displayName()); + safe_user.setDisplayName(updateUserRequest.displayName()); + } + + if (!isIgnorableString(updateUserRequest.password())) { + userRepository.updatePassword(safe_id, updateUserRequest.password()); + safe_user.setPassword(updateUserRequest.password()); + } + + return safe_user; + } + + @Transactional + public UserModel deleteUser(final UUID contextUserId, final UUID subjectUserId) throws WebApplicationException { + final UserModel user = getUser(contextUserId, subjectUserId); + + logger.info("Deleting user " + subjectUserId + " as user " + contextUserId); + + if (!projectRepository.getAllOwnedProjects(user).isEmpty()) { + logger.error("User deletion for user " + subjectUserId + " because user has existing projects"); + + ErrorsCode.UNAUTHORIZED.throwException("A user cannot be removed if they still own projects"); + } + + userRepository.deleteUser(user.getId()); + + return user; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/ErrorsCode.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/ErrorsCode.java new file mode 100644 index 0000000..cf0f8da --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/ErrorsCode.java @@ -0,0 +1,36 @@ +package fr.epita.assistants.ping.errors; + +import fr.epita.assistants.ping.utils.HttpError; +import fr.epita.assistants.ping.utils.IHttpError; +import jakarta.ws.rs.core.Response.Status; +import lombok.Getter; + + +@Getter +public enum ErrorsCode implements IHttpError { + EXAMPLE_ERROR(Status.BAD_REQUEST, "Example error: %s"), + BAD_REQUEST(Status.BAD_REQUEST, "Bad request: %s"), + UNAUTHORIZED(Status.UNAUTHORIZED, "Unauthorized: %s"), + FORBIDDEN(Status.FORBIDDEN, "Forbidden: %s"), + NOT_FOUND(Status.NOT_FOUND, "Not found: %s"), + CONFLICT(Status.CONFLICT, "Conflict: %s"), + INTERNAL_ERROR(Status.INTERNAL_SERVER_ERROR, "Internal server error: %s"), + NOT_IMPLEMENTED(Status.NOT_IMPLEMENTED, "Not implemented") + ; + + private final HttpError error; + + ErrorsCode(Status status, String message) { + error = new HttpError(status, message); + } + + @Override + public RuntimeException get(Object... args) { + return error.get(args); + } + + @Override + public void throwException(Object... args) { + throw error.get(args); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidCommandException.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidCommandException.java new file mode 100644 index 0000000..ad5abe7 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidCommandException.java @@ -0,0 +1,11 @@ +package fr.epita.assistants.ping.errors; + +import fr.epita.assistants.ping.domain.executor.FeatureExecutor; + +import java.security.InvalidParameterException; + +public class InvalidCommandException extends InvalidParameterException { + public InvalidCommandException(FeatureExecutor feature, String command) { + super("Invalid " + feature.name() + " command: " + command); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidFeatureException.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidFeatureException.java new file mode 100644 index 0000000..37a594c --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/InvalidFeatureException.java @@ -0,0 +1,9 @@ +package fr.epita.assistants.ping.errors; + +import java.security.InvalidParameterException; + +public class InvalidFeatureException extends InvalidParameterException { + public InvalidFeatureException(final String feature) { + super("Invalid feature name: " + feature); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/PingServiceError.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/PingServiceError.java new file mode 100644 index 0000000..4a6cf6a --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/PingServiceError.java @@ -0,0 +1,25 @@ +package fr.epita.assistants.ping.errors; + +import lombok.Getter; + +@Getter +public class PingServiceError extends RuntimeException { + private final Integer code; + private final String message; + + public PingServiceError(Integer code, String message) { + super(message); + this.code = code; + this.message = message; + } + + public String toJSON() + { + return "{\"error\":"+code+",\"message\":\""+message+"\"}"; + } + + @Override + public String toString() { + return toJSON(); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/errors/UnreacheableCodeException.java b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/UnreacheableCodeException.java new file mode 100644 index 0000000..eb7c1e8 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/errors/UnreacheableCodeException.java @@ -0,0 +1,7 @@ +package fr.epita.assistants.ping.errors; + +public class UnreacheableCodeException extends RuntimeException { + public UnreacheableCodeException() { + super("Unreacheable code reached during execution"); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/DebugResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/DebugResource.java new file mode 100644 index 0000000..9b9ffd9 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/DebugResource.java @@ -0,0 +1,64 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.data.repository.UserRepository; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import javax.sql.DataSource; +import java.sql.Connection; + +@Path("/api") +public class DebugResource { + + @Inject + DataSource dataSource; + @Inject + UserRepository userRepository; + + // c bo l'html dans les commentaires :) + /** + *

+ * GET /dbstatus
+ * Returns 200 {"status":"OK"} if connected to DB
+ * Otherwise 400 {"status":"KO","reason":"brr brr patapim (raison de l'erreur)"} + *

+ */ + @GET + @Path("/dbstatus") + @Produces(MediaType.APPLICATION_JSON) + public Response dbStatus() { + try (Connection conn = dataSource.getConnection()) { + if (conn.isValid(2)) { + return Response.ok("{\"status\":\"OK\"}").build(); + } else { + return Response.status(400).entity("{\"status\":\"KO\",\"reason\":\"Connection not valid\"}").build(); + } + } catch(Exception e) { + return Response.status(400).entity("{\"status\":\"KO\",\"reason\":\"" + e.getMessage() + "\"}").build(); + } + } + + @GET + @Path("/dbdebuguser") + @Produces(MediaType.APPLICATION_JSON) + @Transactional + public Response dbDebugUser() { + try { + if(userRepository.getUser("admin").isPresent() && userRepository.getUser("brrbrr").isPresent()) + return Response.ok("{\"created\":false}").build(); + var u1 = userRepository.createUser("admin",true,"/img/default-avatar.png","admin","admin"); + var u2 = userRepository.createUser("brrbrr",false,"/img/default-avatar.png","brrbrr","brrbrr"); + userRepository.persist(u1); + userRepository.persist(u2); + return Response.ok("{\"created\":true}").build(); + } catch (Exception e){ + return Response.status(409).entity("{\"error\":true}").build(); + } + + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/HelloWorldResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/HelloWorldResource.java new file mode 100644 index 0000000..7dc29dc --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/HelloWorldResource.java @@ -0,0 +1,27 @@ +package fr.epita.assistants.ping.presentation.rest; + +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import static fr.epita.assistants.ping.errors.ErrorsCode.EXAMPLE_ERROR; + +@Path("/api") +public class HelloWorldResource { + + @GET + @Path("/hello") + @Produces(MediaType.TEXT_PLAIN) + public Response helloWorld() { + return Response.ok("Hello World !").build(); + } + + @GET + @Path("/error") + @Produces(MediaType.APPLICATION_JSON) + public Response error() { + EXAMPLE_ERROR.throwException("This is an error"); + // This line will never be reached + return Response.noContent().build(); + } +} \ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/TransactionResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/TransactionResource.java new file mode 100644 index 0000000..1cde188 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/TransactionResource.java @@ -0,0 +1,115 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.api.requests.NewTransactionRequest; +import fr.epita.assistants.ping.api.requests.UpdateTransactionRequest; +import fr.epita.assistants.ping.api.responses.EcoScoreResponse; +import fr.epita.assistants.ping.api.responses.TransactionResponse; +import fr.epita.assistants.ping.api.responses.TransactionsResponse; +import fr.epita.assistants.ping.converters.request.TransactionCreationEntityConverter; +import fr.epita.assistants.ping.converters.request.TransactionUpdateEntityConverter; +import fr.epita.assistants.ping.converters.response.TransactionResponseConverter; +import fr.epita.assistants.ping.domain.service.TransactionService; +import fr.epita.assistants.ping.errors.ErrorsCode; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.UUID; + +@Path("/api/transactions") +@Authenticated +public class TransactionResource { + + @Inject + JsonWebToken jwt; + + @Inject + TransactionCreationEntityConverter transactionCreationEntityConverter; + + @Inject + TransactionUpdateEntityConverter transactionUpdateEntityConverter; + + @Inject + TransactionResponseConverter transactionResponseConverter; + + @Inject + TransactionService transactionService; + + @GET + public TransactionsResponse getAllTransactions() { + return new TransactionsResponse( + transactionResponseConverter.convert( + transactionService.getAllTransactions(UUID.fromString(jwt.getSubject())) + ) + ); + } + + @POST + public Response createNewTransaction(final NewTransactionRequest request) { + try { + return Response.status(201).entity(transactionResponseConverter.convert( + transactionService.createTransaction( + UUID.fromString(jwt.getSubject()), + transactionCreationEntityConverter.convert(request) + ) + )).build(); + } catch (IllegalArgumentException e) { + ErrorsCode.BAD_REQUEST.throwException("Currency is invalid"); + } + + return null; + } + + @Path("eco-score") + @GET + public EcoScoreResponse getTransactionsEcoScore() { + return new EcoScoreResponse( + transactionService.getAverageEcoScore( + UUID.fromString(jwt.getSubject()) + ) + ); + } + + @Path("{id}") + @GET + public TransactionResponse getTransaction(@PathParam("id") final UUID id) { + return transactionResponseConverter.convert( + transactionService.getTransaction( + UUID.fromString(jwt.getSubject()), + id + ) + ); + } + + @Path("{id}") + @PUT + public TransactionResponse updateTransaction(@PathParam("id") final UUID id, + final UpdateTransactionRequest request) { + try { + return transactionResponseConverter.convert( + transactionService.updateTransaction( + UUID.fromString(jwt.getSubject()), + id, + transactionUpdateEntityConverter.convert(request) + ) + ); + } catch (IllegalArgumentException e) { + ErrorsCode.BAD_REQUEST.throwException("Currency is invalid"); + } + + return null; + } + + @Path("{id}") + @DELETE + public TransactionResponse deleteTransaction(@PathParam("id") final UUID id) { + return transactionResponseConverter.convert( + transactionService.deleteTransaction( + UUID.fromString(jwt.getSubject()), + id + ) + ); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/UtilsResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/UtilsResource.java new file mode 100644 index 0000000..1661b96 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/UtilsResource.java @@ -0,0 +1,19 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.api.responses.EcoScoreResponse; +import fr.epita.assistants.ping.errors.ErrorsCode; +import io.quarkus.security.Authenticated; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/api") +public class UtilsResource { + + @Path("/eco-score") + @GET + @Authenticated + public EcoScoreResponse getGlobalEcoScore() { + ErrorsCode.NOT_IMPLEMENTED.throwException(); + return null; + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FileResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FileResource.java new file mode 100644 index 0000000..a278b2b --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FileResource.java @@ -0,0 +1,108 @@ +package fr.epita.assistants.ping.presentation.rest.fileSystemEndpoints; + +import fr.epita.assistants.ping.domain.service.FileSystemService; + +import fr.epita.assistants.ping.api.requests.MoveRequest; +import fr.epita.assistants.ping.api.requests.PathRequest; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.security.Authenticated; +import io.smallrye.jwt.util.ResourceUtils; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.io.InputStream; +import java.util.UUID; + +@Path("/api/projects") +public class FileResource +{ + @Inject + JsonWebToken jwt; + + @Inject + FileSystemService fileSystemService; + + @Inject + Logger logger; + + // Seuls les membres du projet et les admins y ont access + // Quand on renvoit du octet stream: + @GET + @Path("/{projectId}/files") + @Authenticated + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Response getFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + @QueryParam("path") String path) + { + //return Response.ok("Some stuff that is in the file").build(); + logger.log("Entering endpoint 'getFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | (Query)path: " + path); + return fileSystemService.readFile(UUID.fromString(jwt.getSubject()), id, new PathRequest(path)); + } + + // Seuls les membres du projet et les admins y ont access + @DELETE + @Path("/{projectId}/files") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response deleteFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + PathRequest path) + { + //return Response.ok().build(); + logger.log("Entering endpoint 'deleteFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path.toString()); + return fileSystemService.deleteFile(UUID.fromString(jwt.getSubject()), id, path); + } + + // Seuls les membres du projet et les admins y ont access + @POST + @Path("/{projectId}/files") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response createFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + PathRequest path) + { + //return Response.ok().build(); + logger.log("Entering endpoint 'createFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path.toString()); + return fileSystemService.createFile(UUID.fromString(jwt.getSubject()), id, path); + } + + // Seuls les membres du projet et les admins y ont access + @PUT + @Path("/{projectId}/files/move") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response moveFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + MoveRequest request) + { + //return Response.ok().build(); + logger.log("Entering endpoint 'moveFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | request: " + request.toString()); + return fileSystemService.moveFile(UUID.fromString(jwt.getSubject()), id, request); + } + + // Seuls les membres du projet et les admins y ont access + // Probleme, beaucoup de sources semblent dire que consommer de l'application octet stream + // Necessite de lire/utiliser une variable speciale + // Aucune idee de ce que c'est ou comment ca marche + @POST + @Path("/{projectId}/files/upload") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_OCTET_STREAM) + public Response uploadFile(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + @QueryParam("path") String path, + InputStream content) + { + logger.log("Entering endpoint 'uploadFile' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path + " | content: " + content.toString()); + return fileSystemService.uploadFile(UUID.fromString(jwt.getSubject()), id, path, content); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FolderResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FolderResource.java new file mode 100644 index 0000000..ee8e319 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/fileSystemEndpoints/FolderResource.java @@ -0,0 +1,80 @@ +package fr.epita.assistants.ping.presentation.rest.fileSystemEndpoints; + +import fr.epita.assistants.ping.api.requests.MoveRequest; +import fr.epita.assistants.ping.api.requests.PathRequest; +import fr.epita.assistants.ping.domain.service.FileSystemService; + +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.UUID; + +@Path("/api/projects") +public class FolderResource +{ + @Inject + JsonWebToken jwt; + + @Inject + FileSystemService fileSystemService; + + @Inject + Logger logger; + + // Seuls les membres du projet et les admins y ont access + @GET + @Path("/{projectId}/folders") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response listFolder(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + @QueryParam("path") String path) + { + logger.log("Entering endpoint 'listFolder' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | (Query)path: " + path); + return fileSystemService.listFolder(UUID.fromString(jwt.getSubject()), id, new PathRequest(path)); + } + + // Seuls les membres du projet et les admins y ont access + @DELETE + @Path("/{projectId}/folders") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response deleteFolder(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + PathRequest path) + { + logger.log("Entering endpoint 'deleteFolder' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path.toString()); + return fileSystemService.deleteFolder(UUID.fromString(jwt.getSubject()), id, path); + } + + @POST + @Path("/{projectId}/folders") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response createFolder(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + PathRequest path) + { + logger.log("Entering endpoint 'createFolder' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | path: " + path.toString()); + return fileSystemService.createFolder(UUID.fromString(jwt.getSubject()), id, path); + } + + @PUT + @Path("/{projectId}/folders/move") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response moveFolder(@Context SecurityContext ctx, + @PathParam("projectId") final UUID id, + MoveRequest request) + { + logger.log("Entering endpoint 'moveFolder' with parameters: | ctx: " + ctx.toString() + " | (Path)projectId: " + id.toString() + " | request: " + request.toString()); + return fileSystemService.moveFolder(UUID.fromString(jwt.getSubject()), id, request); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/projectEndpoints/ProjectResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/projectEndpoints/ProjectResource.java new file mode 100644 index 0000000..2d9a712 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/projectEndpoints/ProjectResource.java @@ -0,0 +1,154 @@ +package fr.epita.assistants.ping.presentation.rest.projectEndpoints; + +import fr.epita.assistants.ping.api.requests.ExecFeatureRequest; +import fr.epita.assistants.ping.api.requests.NewProjectRequest; +import fr.epita.assistants.ping.api.requests.UpdateProjectRequest; +import fr.epita.assistants.ping.api.requests.UserProjectRequest; +import fr.epita.assistants.ping.api.responses.ProjectResponse; +import fr.epita.assistants.ping.errors.ErrorsCode; +import fr.epita.assistants.ping.errors.PingServiceError; +import fr.epita.assistants.ping.domain.service.ProjectService; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.security.Authenticated; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.List; +import java.util.UUID; + +@Path("/api/projects") +public class ProjectResource { + @Inject + JsonWebToken jwt; + + @Inject + ProjectService projectService; + + @Inject + Logger logger; + + // Si le QueryParam onlyOwned n'est pas precise, le considerer faux + @GET + @Path("/") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response getMyProject(@Context SecurityContext ctx, @QueryParam("onlyOwned") Boolean owned) { + logger.log("Entering endpoint 'getMyProject' with parameters: | ctx: " + ctx.toString() + " | (QuerryParam) onlyOwned: " + ((owned) != null && owned)); + List ls = projectService.listProjectsOfUser(UUID.fromString(jwt.getSubject()), owned); + return Response.ok(ls).build(); + } + + @POST + @Path("/") + @Authenticated + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response createProject(@Context SecurityContext ctx, NewProjectRequest project) { + if(project == null || ctx == null) { + logger.error("Entering endpoint with 'createProject' (it will fail cuz project or ctx is null)"); + ErrorsCode.BAD_REQUEST.throwException("Invalid project creation form"); + } + + logger.log("Entering endpoint 'createProject' with parameters: | ctx: " + ctx.toString() + " | project: " + project.toString()); + ProjectResponse pr = projectService.createProject(UUID.fromString(jwt.getSubject()), project.getName()); + return Response.ok(pr).build(); + } + + @GET + @Path("/all") + @RolesAllowed({"admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Response allProject(@Context SecurityContext ctx) { + logger.log("Entering endpoint 'allProject' with parameters: | ctx: " + ctx.toString()); + List ls = projectService.listAllProjects(); + return Response.ok(ls).build(); + + } + + // Seul un admin ou le detenteur du projet a le droit de call ça + @PUT + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response updateProject(@Context SecurityContext ctx, + @PathParam("id") final String id, + final UpdateProjectRequest update) { + logger.log("Entering endpoint 'updateProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id + " | update: " + update.toString()); + ProjectResponse res = projectService.updateProject(UUID.fromString(jwt.getSubject()), id, update.getName(), update.getNewOwnerId()); + return Response.ok(res).build(); + + } + + // User y accede s'il est owner ou membre du projet + @GET + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response getProject(@Context SecurityContext ctx, @PathParam("id") final String id) { + logger.log("Entering endpoint 'getProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id); + ProjectResponse res = projectService.getProject(UUID.fromString(jwt.getSubject()), id); + return Response.ok(res).build(); + + } + + // Seul admin et le detenteur du projet y ont acces + @DELETE + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response deleteProject(@Context SecurityContext ctx, @PathParam("id") final String id) { + logger.log("Entering endpoint 'deleteProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id); + projectService.deleteProject(UUID.fromString(jwt.getSubject()), id); + return Response.noContent().build(); + } + + // Admin and members of the project can access this + @POST + @Path("/{id}/add-user") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response addUserProject(@Context SecurityContext ctx, + @PathParam("id") final String id, + final UserProjectRequest project) { + logger.log("Entering endpoint 'addUserProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id + " | project: " + project.toString()); + projectService.addMember(UUID.fromString(jwt.getSubject()), id, project.getUserId()); + return Response.noContent().build(); + } + + // Admin and members of the project can access this + @POST + @Path("/{id}/exec") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response execOnProject(@Context SecurityContext ctx, + @PathParam("id") final String id, + final ExecFeatureRequest request) { + logger.log("Entering endpoint 'execOnProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id + " | request: " + request.toString()); + + projectService.execute( + UUID.fromString(jwt.getSubject()), UUID.fromString(id), request.getFeature(), request.getCommand(), + request.getParams() + ); + + return Response.noContent().build(); + + } + + @POST + @Path("/{id}/remove-user") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response removeUserProject(@Context SecurityContext ctx, + @PathParam("id") final String id, + final UserProjectRequest project) { + logger.log("Entering endpoint 'updateProject' with parameters: | ctx: " + ctx.toString() + " | (PathParam) id: " + id + " | project: " + project.toString()); + projectService.removeMember(UUID.fromString(jwt.getSubject()), id, project.getUserId()); + return Response.noContent().build(); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/userEndpoints/UserResource.java b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/userEndpoints/UserResource.java new file mode 100644 index 0000000..6b7ea77 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/presentation/rest/userEndpoints/UserResource.java @@ -0,0 +1,149 @@ +package fr.epita.assistants.ping.presentation.rest.userEndpoints; + +import fr.epita.assistants.ping.api.requests.LoginRequest; +import fr.epita.assistants.ping.api.requests.NewUserRequest; +import fr.epita.assistants.ping.api.requests.UpdateUserRequest; +import fr.epita.assistants.ping.api.responses.LoginResponse; +import fr.epita.assistants.ping.converters.request.UserCreationEntityConverter; +import fr.epita.assistants.ping.converters.request.UserUpdateEntityConverter; +import fr.epita.assistants.ping.converters.response.UserResponseConverter; +import fr.epita.assistants.ping.domain.service.UserService; +import fr.epita.assistants.ping.utils.Logger; +import io.quarkus.security.Authenticated; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + +import java.util.UUID; + +@Path("/api/user") +public class UserResource +{ + @Inject + JsonWebToken jwt; + + @Inject + UserService userService; + + @Inject + Logger logger; + + @Inject + UserResponseConverter userResponseConverter; + + @Inject + UserCreationEntityConverter userCreationEntityConverter; + + @Inject + UserUpdateEntityConverter userUpdateEntityConverter; + + // Si on n'a pas de token/token invalide, alors le RolesAllowed + // vont renvoyer un 401. + // Je suppose que ne pas etre admin va renvoyer 403 si le jwt est bien fait + @POST + @Path("/") + @RolesAllowed({"admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Response createUser(@Context SecurityContext ctx, NewUserRequest user) + { + logger.log("Entering endpoint 'createUser' with parameters: | ctx: " + ctx.toString() + " | user: " + user.toString()); + return Response + .ok(userResponseConverter.convert( + userService.createUser( + UUID.fromString(jwt.getSubject()), + userCreationEntityConverter.convert(user) + ))) + .build(); + } + + @GET + @Path("/all") + @RolesAllowed({"admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Response allUsers(@Context SecurityContext ctx) + { + logger.log("Entering endpoint 'allUser' with parameters: | ctx: " + ctx.toString()); + return Response + .ok(userResponseConverter.convert(userService.getAllUsers())) + .build(); + } + + // Le seul endpoint qui ne sera pas protege + @POST + @Path("/login") + @Produces(MediaType.APPLICATION_JSON) + public Response logInUser(final LoginRequest request) + { + logger.log("Entering endpoint 'logInUser' with parameters: | request: " + request.toString()); + return Response + .ok(new LoginResponse(userService.login(request.getLogin(), request.getPassword()))) + .build(); + } + + @GET + @Path("/refresh") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response refreshToken(@Context SecurityContext ctx) + { + logger.log("Entering endpoint 'refreshToken' with parameters: | ctx: " + ctx.toString()); + return Response + .ok(new LoginResponse(userService.refreshToken(UUID.fromString(jwt.getSubject())))) + .build(); + } + + // Admin peut tout toucher + // User peut toucher que a son profil + @PUT + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response updateUser(@Context SecurityContext ctx, + @PathParam("id") final UUID id, + UpdateUserRequest user) + { + logger.log("Entering endpoint 'updateUser' with parameters: | ctx: " + ctx.toString() + " | (Path) id: " + id.toString() + " | user: " + user.toString()); + return Response + .ok(userResponseConverter.convert( + userService.updateUser( + UUID.fromString(jwt.getSubject()), + id, + userUpdateEntityConverter.convert(user)) + )) + .build(); + } + + // Admin peut tout toucher + // User peut toucher que a son profil + @GET + @Path("/{id}") + @Authenticated + @Produces(MediaType.APPLICATION_JSON) + public Response getUser(@Context SecurityContext ctx, @PathParam("id") final UUID id) + { + logger.log("Entering endpoint 'getUser' with parameters: | ctx: " + ctx.toString() + " | (Path) id: " + id.toString()); + return Response + .ok(userResponseConverter.convert(userService.getUser(UUID.fromString(jwt.getSubject()), id))) + .build(); + } + + // Seul Admin peut delete + // Cependant un user avec des projects ne peut pas etre delete + @DELETE + @Path("/{id}") + @RolesAllowed({"admin"}) + @Produces(MediaType.APPLICATION_JSON) + public Response deleteUser(@Context SecurityContext ctx, @PathParam("id") final UUID id) + { + logger.log("Entering endpoint 'getUser' with parameters: | ctx: " + ctx.toString() + " | (Path) id: " + id.toString()); + return Response + .status(204) + .entity(userResponseConverter.convert(userService.deleteUser(UUID.fromString(jwt.getSubject()), id))) + .build(); + } +} \ No newline at end of file diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/ErrorInfo.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/ErrorInfo.java new file mode 100644 index 0000000..0255e86 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/ErrorInfo.java @@ -0,0 +1,14 @@ +package fr.epita.assistants.ping.utils; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@AllArgsConstructor +@Setter +@NoArgsConstructor +public class ErrorInfo { + private String message; +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/HttpError.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/HttpError.java new file mode 100644 index 0000000..3ec0c86 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/HttpError.java @@ -0,0 +1,40 @@ +package fr.epita.assistants.ping.utils; + +import java.util.function.Supplier; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response.Status; + +import static jakarta.ws.rs.core.Response.status; + +public class HttpError implements Supplier, IHttpError { + private final Status status; + private final String message; + + public HttpError(Status status, String message) { + this.status = status; + this.message = message; + } + + private RuntimeException createError(Object... args) { + return new WebApplicationException( + status(status) + .entity(new ErrorInfo(String.format(message, args))) + .build()); + } + + @Override + public RuntimeException get() { + return createError(); + } + + @Override + public RuntimeException get(Object... args) { + return createError(args); + } + + @Override + public void throwException(Object... args) { + throw get(args); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/IHttpError.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/IHttpError.java new file mode 100644 index 0000000..6616166 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/IHttpError.java @@ -0,0 +1,7 @@ +package fr.epita.assistants.ping.utils; + +public interface IHttpError { + RuntimeException get(Object... args); + + void throwException(Object... args); +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/Logger.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/Logger.java new file mode 100644 index 0000000..a3010b3 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/Logger.java @@ -0,0 +1,129 @@ +package fr.epita.assistants.ping.utils; + +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.Console; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Calendar; + +@ApplicationScoped +public class Logger { + private static final String RESET_TEXT = "\u001B[0m"; + private static final String RED_TEXT = "\u001B[31m"; + private static final String GREEN_TEXT = "\u001B[32m"; + private static final String YELLOW_TEXT = "\u001B[33m"; + private static final String BLUE_TEXT = "\u001B[36m"; + private static final String PURPLE_TEXT = "\u001B[35m"; + + @ConfigProperty(name="LOG_FILE", defaultValue="ping.log") + String logFile; + + @ConfigProperty(name="ERROR_LOG_FILE", defaultValue="error.log") + String errorFile; + + // null = file inexistante + private static File logPath = null; + private static File errorPath = null; + + public void checkFiles() + { + if (logPath == null) + { + logPath = new File(logFile); + + try + { + logPath.createNewFile(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + } + } + + if (errorPath == null) + { + errorPath = new File(errorFile); + try + { + errorPath.createNewFile(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + } + } + } + + public void log(String text) + { + text = "[" + timestamp() + "] [LOG] " + text; + writeToFile("normal", text); + System.out.println(PURPLE_TEXT + text + RESET_TEXT); + } + public void debug(String text) + { + text = "[" + timestamp() + "] [DEBUG] " + text; + writeToFile("normal", text); + System.out.println(BLUE_TEXT + text + RESET_TEXT); + } + public void info(String text) + { + text = "[" + timestamp() + "] [INFO] " + text; + writeToFile("normal", text); + System.out.println(text); + } + public void warn(String text) + { + text = "[" + timestamp() + "] [!WARN!] " + text; + writeToFile("normal", text); + System.out.println(YELLOW_TEXT + text + RESET_TEXT); + } + public void error(String text) + { + text = "[" + timestamp() + "] [!!!ERROR!!!] " + text; + writeToFile("error", text); + System.out.println(RED_TEXT + text + RESET_TEXT); + } + + private void writeToFile(String type, String text) + { + checkFiles(); + if (type.equals("normal") && logFile != null) + { + try + { + // On est en Java, j'en ai rien a faire + FileWriter writer = new FileWriter(logFile, true); + writer.write(text + "\n"); + writer.close(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + } + } + if (!type.equals("normal") && errorFile != null) + { + try + { + FileWriter writer = new FileWriter(errorFile, true); + writer.write(text + "\n"); + writer.close(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + } + } + } + + private static String timestamp() { + return new SimpleDateFormat("dd/MM/yy - HH:mm:ss") + .format(Calendar.getInstance().getTime()); + } +} diff --git a/ping/ping/src/main/java/fr/epita/assistants/ping/utils/TokenFactory.java b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/TokenFactory.java new file mode 100644 index 0000000..1059733 --- /dev/null +++ b/ping/ping/src/main/java/fr/epita/assistants/ping/utils/TokenFactory.java @@ -0,0 +1,27 @@ +package fr.epita.assistants.ping.utils; + +import fr.epita.assistants.ping.data.model.UserModel; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.build.JwtClaimsBuilder; +import jakarta.enterprise.context.ApplicationScoped; + + +@ApplicationScoped +public class TokenFactory { + + private String deduceTokenRole(UserModel user) { + return user.getIsAdmin() ? "admin" : "user"; + } + + private JwtClaimsBuilder buildClaims(UserModel user) { + return Jwt.claims() + .subject(user.getId().toString()) + .upn(user.getLogin()) + .preferredUserName(user.getLogin()) + .groups(deduceTokenRole(user)); + } + + public String generateLoginToken(UserModel user) { + return buildClaims(user).sign(); + } +} diff --git a/ping/ping/src/main/resources/application.properties b/ping/ping/src/main/resources/application.properties new file mode 100644 index 0000000..31ac487 --- /dev/null +++ b/ping/ping/src/main/resources/application.properties @@ -0,0 +1,21 @@ +# Enable HTTP +quarkus.http.insecure-requests=enabled +# Setup PostgreSQL +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=postgres +quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/ping +quarkus.transaction-manager.default-transaction-timeout=3000s +quarkus.hibernate-orm.log.queries-slower-than-ms=200 +quarkus.hibernate-orm.database.generation=update +# Set port +quarkus.http.port=8080 +quarkus.http.cors.enabled=false +quarkus.devservices.enabled=true +%test.quarkus.devservices.enabled=false +# JWT configuration +smallrye.jwt.new-token.signature-algorithm=RS256 +smallrye.jwt.sign.key.location=privateKey.pem +smallrye.jwt.verify.key.location=publicKey.pem +smallrye.jwt.new-token.lifespan=3600 +smallrye.jwt.new-token.issuer=PatapimVest-API +PROJECT_DEFAULT_PATH=/var/www/projects/ \ No newline at end of file diff --git a/ping/ping/src/main/resources/openapi.yml b/ping/ping/src/main/resources/openapi.yml new file mode 100644 index 0000000..104ba5e --- /dev/null +++ b/ping/ping/src/main/resources/openapi.yml @@ -0,0 +1,1065 @@ +--- +openapi: 3.1.0 +paths: + /api/error: + get: + responses: + "200": + description: OK + summary: Error + tags: + - Hello World Resource + /api/hello: + get: + responses: + "200": + description: OK + summary: Hello World + tags: + - Hello World Resource + /api/projects: + get: + summary: List projects of user + description: | + List all the projects of the logged in user. + + The param onlyOwned specifies if the response should return all the projects the user is a member of, + or only those the user owns. + + By default onlyOwned is considered as false. + + Any logged in user can access this endpoint. + tags: + - Project + parameters: + - name: onlyOwned + in: query + schema: + type: boolean + responses: + "200": + description: List of the projects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectResponse' + "403": + description: Should not happen (this is automatically generated) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + post: + summary: Create a project + description: | + Create a new project with a name, the owner should also be in the list of members. + + Any logged in user can access this endpoint. + tags: + - Project + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewProjectRequest' + required: true + responses: + "200": + description: The created project + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectResponse' + "400": + description: The project name is invalid (null or empty for example) + "403": + description: Should not happen (this is automatically generated) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/all: + get: + summary: List all the projects + description: | + List all the projects available, be careful the response should not contain all the information about the members or the owner. + + Only an admin can access this endpoint. + tags: + - Project + responses: + "200": + description: List of the projects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectResponse' + "403": + description: The user is not an admin + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: + - admin + /api/projects/{id}: + put: + summary: Update a project + description: | + Update a project, by changing its name or the owner of the project. + + If the new owner id or the new name are the same as the current one, it should not change anything and it is not an error. + + You should only update the fields that are not null in the request. + If some fields are null, they should not be updated. + + If all the fields are null, it should return a bad request. + + Only the owner of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProjectRequest' + required: true + responses: + "200": + description: The project was modified + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectResponse' + "404": + description: "The project could not be found, or the new owner is not a\ + \ member of the project" + "403": + description: The user is not allowed to access the project + "400": + description: Both the name and the new owner are null + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + get: + summary: Get a project + description: |- + Get a specific project based on its id. + + A user can access this endpoint if he is a member of the project or the owner. + + An admin can access this endpoint in any case. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + responses: + "200": + description: The project + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectResponse' + "404": + description: Project not found + "403": + description: The user is not allowed to access the project + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + delete: + summary: Delete a project + description: | + Delete a project. + + Only the owner of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + responses: + "204": + description: The project was deleted + "404": + description: The project could not be found + "403": + description: The user is not allowed to access this project + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{id}/add-user: + post: + summary: Add a member + description: | + Add a member to a project. + + Any member of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserProjectRequest' + required: true + responses: + "204": + description: The user was added to the project + "404": + description: "The project, or the user could not be found" + "403": + description: The user is not allowed to access the project + "400": + description: The userId is invalid (null or empty for example) + "409": + description: The user is already a member of the project + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{id}/exec: + post: + summary: Execute a feature + description: | + Execute a feature on the project. + + For git features: + + If the project is not a git repository, it should return a bad request (except if the feature is git init). + + + For git add, if a file does not exist it should return a bad request. + + Any other error should be returned as an internal server error. + + Any member of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ExecFeatureRequest' + required: true + responses: + "204": + description: The feature was successfully executed + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project + "400": + description: "Any parameter of the request is invalid (null or non-existent\ + \ feature)" + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{id}/remove-user: + post: + summary: Remove a member + description: | + Remove a member from a project. + + Only the owner of the project or an admin can access this endpoint. + tags: + - Project + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserProjectRequest' + required: true + responses: + "204": + description: The user was removed from the project + "404": + description: "The project, or the user could not be found, the user to remove\ + \ is not a member of the project" + "403": + description: "The user is not allowed to access the project, or the user\ + \ to remove is the owner of the project" + "400": + description: The userId is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/files: + get: + summary: Get a file + description: | + Retrieve the content of a file. + + It should return a byte array of the content of the file. + + The content type must be application/octet-stream. + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + - name: path + in: query + schema: + type: string + default: "" + responses: + "200": + description: Content of the file + "404": + description: The project or the relative path could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + delete: + summary: Delete a file + description: | + Delete a file from the file system and all its content, be careful if the file is the root + you should only empty it and not remove the root file of the project. + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PathRequest' + required: true + responses: + "204": + description: The file was deleted + "404": + description: The project or the file could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "400": + description: The source or destination path is invalid (null or empty for + example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + post: + summary: Create a file + description: | + Create a new file on the file system. + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PathRequest' + required: true + responses: + "201": + description: The file was created + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "409": + description: The file already exists + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/files/move: + put: + summary: Move a file + description: | + Move a file to a new location or rename it if the destination is in the same parent directory + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MoveRequest' + required: true + responses: + "204": + description: The file was renamed + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "409": + description: The file already exists + "400": + description: The source or destination path is invalid (null or empty for + example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/files/upload: + post: + summary: Upload a new file + description: | + Upload data to create or modify a file, it should write the content + of the body to the file and create it if it does not exist. + + The content type received must be application/octet-stream. + + You can retrieve the data of the body using the InputStream type for your parameter. + + Any member of the project or an admin can access this endpoint. + tags: + - File + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + - name: path + in: query + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + required: true + responses: + "201": + description: The file was created + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/folders: + get: + summary: List a folder + description: | + List the content of the folder located at the given path + in the project with the given id. By default it lists the root folder. + + It is NOT a recursive listing, it only lists the immediate children of the folder. + + Any member of the project or an admin can access this endpoint. + tags: + - Folder + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + - name: path + in: query + schema: + type: string + default: "" + responses: + "200": + description: List of folder entries + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/FSEntryResponse' + "404": + description: The project or the relative path could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + delete: + summary: Delete a folder + description: | + Delete a folder from the file system and all its content, + be careful if the folder is the root you should only empty it and not remove the root folder of the project. + + Any member of the project or an admin can access this endpoint. + tags: + - Folder + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PathRequest' + required: true + responses: + "204": + description: The folder was deleted + "404": + description: The project or the folder could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + post: + summary: Create a folder + description: | + Create a new folder on the file system. + + Any member of the project or an admin can access this endpoint. + tags: + - Folder + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PathRequest' + required: true + responses: + "201": + description: The folder was created + "404": + description: The project could not be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "409": + description: The folder already exists + "400": + description: The relative path is invalid (null or empty for example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/projects/{projectId}/folders/move: + put: + summary: Move a folder + description: | + Move a folder to a new location, or rename it if the destination is in the same parent directory. + + Any member of the project or an admin can access this endpoint. + tags: + - Folder + parameters: + - name: projectId + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MoveRequest' + required: true + responses: + "204": + description: The folder was renamed + "404": + description: The project could not be found or the source folder could not + be found + "403": + description: The user is not allowed to access the project or a path traversal + attack was detected + "409": + description: The folder already exists + "400": + description: The source or destination path is invalid (null or empty for + example) + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/user: + post: + summary: Create a user + description: | + Create a new user, its login should be unique and contain at most + one '.' or '_'. The name should be initialized to the login split and capitalized on the separator. + + Only an admin can access this endpoint. + tags: + - User + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewUserRequest' + required: true + responses: + "200": + description: The created user + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + "400": + description: The login or the password is invalid + "409": + description: The login is already taken + "403": + description: The user is not an admin + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: + - admin + /api/user/all: + get: + summary: List all the users + description: | + List all the users available, be careful the response should not contain all the + information about the members or the owner. + + Only an admin can access this endpoint. + tags: + - User + responses: + "200": + description: List of the users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/UserResponse' + "403": + description: The user is not an admin + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: + - admin + /api/user/login: + post: + summary: Login a user + description: | + Login a user with its login and password. + + It should return a token if the login/password combination is valid. + tags: + - User + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + required: true + responses: + "200": + description: The token of the user + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + "401": + description: The login/password combination is invalid + "400": + description: The login or the password is null + servers: + - url: http://localhost:8080 + /api/user/refresh: + get: + summary: Refresh a token + description: | + Refresh the token of a user by returning a new one. + + Only a logged in user can access this endpoint. + tags: + - User + responses: + "200": + description: The new token + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + "404": + description: The user could not be found + "401": + description: Not Authorized + "403": + description: Not Allowed + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + /api/user/{id}: + put: + summary: Update a user + description: | + Update a user + + For the sake of simplicity the password can be modified. + + Any blank value should be ignored (except for the avatar). + + Only the same user or an admin can access this endpoint. + tags: + - User + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateUserRequest' + required: true + responses: + "200": + description: The modified user + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + "404": + description: The user could not be found + "403": + description: The user is not allowed + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + get: + summary: Get a user + description: | + Get the data of a user. + + A user can only access its data while an admin can access any user data. + tags: + - User + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + responses: + "200": + description: The user + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + "404": + description: User not found + "403": + description: The user is not allowed to access this user + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: [] + delete: + summary: Delete a user + description: | + Delete a user. + + If the user owns projects, he cannot be deleted and a forbidden response should be returned. + + Only an admin can access this endpoint. + tags: + - User + parameters: + - name: id + in: path + required: true + schema: + $ref: '#/components/schemas/UUID' + responses: + "204": + description: The user was deleted + "404": + description: The user could not be found + "403": + description: "The user is not allowed to access this endpoint, or the user\ + \ owns projects" + "401": + description: Not Authorized + servers: + - url: http://localhost:8080 + security: + - SecurityScheme: + - admin +components: + schemas: + ExecFeatureRequest: + type: object + properties: + feature: + type: string + command: + type: string + params: + type: array + items: + type: string + FSEntryResponse: + type: object + properties: + name: + type: string + path: + type: string + isDirectory: + type: boolean + LoginRequest: + type: object + properties: + login: + type: string + password: + type: string + LoginResponse: + type: object + properties: + token: + type: string + MoveRequest: + type: object + properties: + src: + type: string + dst: + type: string + NewProjectRequest: + type: object + properties: + name: + type: string + NewUserRequest: + type: object + properties: + login: + type: string + password: + type: string + isAdmin: + type: boolean + PathRequest: + type: object + properties: + relativePath: + type: string + ProjectResponse: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + name: + type: string + members: + type: array + items: + $ref: '#/components/schemas/UserSummaryResponse' + owner: + $ref: '#/components/schemas/UserSummaryResponse' + UUID: + type: string + format: uuid + pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + UpdateProjectRequest: + type: object + properties: + name: + type: string + newOwnerId: + $ref: '#/components/schemas/UUID' + UpdateUserRequest: + type: object + properties: + password: + type: string + displayName: + type: string + avatar: + type: string + UserProjectRequest: + type: object + properties: + userId: + $ref: '#/components/schemas/UUID' + UserResponse: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + login: + type: string + displayName: + type: string + isAdmin: + type: boolean + avatar: + type: string + UserSummaryResponse: + type: object + properties: + id: + $ref: '#/components/schemas/UUID' + displayName: + type: string + avatar: + type: string + securitySchemes: + SecurityScheme: + type: http + scheme: bearer + bearerFormat: JWT + description: Authentication +tags: +- name: File + description: File API +- name: Folder + description: Folder API +- name: Project + description: Project API +- name: User + description: User API +info: + title: ping API + version: "1.0" diff --git a/ping/ping/src/main/resources/privateKey.pem b/ping/ping/src/main/resources/privateKey.pem new file mode 100644 index 0000000..72fef7b --- /dev/null +++ b/ping/ping/src/main/resources/privateKey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQDM3vt0cpH3XNFa +ZpZDkwa6QLV45Z5nB3qd9dF+NgoP+Kzar/VAUndnTaf8YDK4J90bc3jwaepvsE7y +0Wy3KGEwNEDx5A+rIhWFx3UfoPvZLbPzkwpMpCP+tU22OF+EB/mdKRMzl2VX6nOG +fo/P88xmF5wCVTCscM7PQmzEuMTn+MZCnI2N6StrBpJDf2g6lrN3JOq3wpqcIAxP +y2AkrHN2E2Ihut1cbLE9dmhDdyygM2XtzCjFs2foBycFc0u/GpWwqA5VfHs7fww9 +saAtXuBVrZ1A9fvZbXxHbLNVgjsRCoAhk299cRv0h06lamqD/xvfO8UqpIxKAVeQ +3kVReoM/AgMBAAECggEAHnT77wjlWF/tI8JijnM6KdBa6jlAvlJnUTptRZUlE9DF +adGE5WYdKDdsROHMSIJspZsaHKuyVZ5hbkniHqybHj52dT+RcMClcHkBmY0mQ9g+ +i3SNsG0qiMBNaEyjLo689jdYCnB0eYh6WkFQgfrdpUPXRqWBkfFHXZqjbu+FHnMi +VWLspMGetabvEcMDAcDWWrVE+JKuXaBFwJBfXdwNNeo0og4F785YlckymdCoYSBl +9Xv2P7GUilF6PRaNGmSN5gLw9bpJWev7hxPIZK2I+h8fAZOigpPdw0KkpnWFP/Lo +zUT9e4yUeeO2XrWdpY4YEJFcQY1xC47AI62b7pZnoQKBgQDzlGp6DpzANR1frjvS +cwxRqgGssW0ADbuAX7vNDfNGpnbhlSJglWkYIRmP9ylHTjLKz8ju9pmGJ1DU5ADW +B3UBGA3rEOJdqW3pu2Pfc49vZY8XUnUb9iEcjWEIx/bli8md/AoZTb+p+PWHBtOa +EQaicCqYc4HGtLcy40t6rQwNjwKBgQDXUUeK0lkEyEzDDXHqGl1DumMSx5/b+TzO ++GMG00ODNHifO1yNwXZpJ6qwOln6MEVQQxbO0zsWb/fCBa3+nO8QkwJ6RiwL2wZM +LhVT1DgGnGdUHyX8cYrz48KSHT1AaL2Xpso+PEBG57oG06OnTCs4zjmXNhscKQO7 +xp7PQve3UQJ/DkWj94LjrF9wWSrcVnhnQGG1t2it+lLTqbHyTxGCL6GRVaZnXUFj +5wYJeWYCBSuPzlJec6H/XNzw34h8hA3rzSSbrCpRGSvURN21/GHJBb3tpHAwbHL/ +fazLAri0vr5yEc+yx8jGci8kM8UyoNRejyYDWOtrrzjngxPOWGHLUwKBgDdItEDN +j3Ot5h+yxQvhpw6rvaJlxQLa9KUDm5F2DwCSKWJhAHuZL8kCGU5UPSQC3tkj0Khc +DfDN4ORdRvZ33t33kQxKFicZHn3S20EB3Ty9Tb6InN07Xf0O/rc7cK2n9v4IIidm +mu5ZsybrkjpzUSmtsZXEm6NYWJAPCFmZDTdxAoGAPZUhdtFBDHcq1lexfTZuLQXg +/V0aSDO8ptagb/foHuWEnqxwsldMplMjx3naOkLrRYA+BJ/zdItv0cJrbl5xpXI4 +S5AHyerNXzZr3ptYGwOUyulNsmA+9WGBTdREhDnzToFL4u0ZZn5+ENJFPK8rygqh +M6PGLdqKHiXGAsGQz18= +-----END PRIVATE KEY----- diff --git a/ping/ping/src/main/resources/publicKey.pem b/ping/ping/src/main/resources/publicKey.pem new file mode 100644 index 0000000..49676c3 --- /dev/null +++ b/ping/ping/src/main/resources/publicKey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzN77dHKR91zRWmaWQ5MG +ukC1eOWeZwd6nfXRfjYKD/is2q/1QFJ3Z02n/GAyuCfdG3N48Gnqb7BO8tFstyhh +MDRA8eQPqyIVhcd1H6D72S2z85MKTKQj/rVNtjhfhAf5nSkTM5dlV+pzhn6Pz/PM +ZhecAlUwrHDOz0JsxLjE5/jGQpyNjekrawaSQ39oOpazdyTqt8KanCAMT8tgJKxz +dhNiIbrdXGyxPXZoQ3csoDNl7cwoxbNn6AcnBXNLvxqVsKgOVXx7O38MPbGgLV7g +Va2dQPX72W18R2yzVYI7EQqAIZNvfXEb9IdOpWpqg/8b3zvFKqSMSgFXkN5FUXqD +PwIDAQAB +-----END PUBLIC KEY----- diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/ProjectFakeRepository.java b/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/ProjectFakeRepository.java new file mode 100644 index 0000000..d9a1e69 --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/ProjectFakeRepository.java @@ -0,0 +1,111 @@ +package fr.epita.assistants.ping.data.repositories; + +import fr.epita.assistants.ping.data.model.ProjectModel; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.ProjectRepository; + +import java.nio.file.Path; +import java.util.*; + + +public class ProjectFakeRepository extends ProjectRepository { + + private final Path defaultPath = Path.of("."); + + private final Map database = new HashMap<>(); + + @Override + public long count() { + return database.size(); + } + + @Override + public ProjectModel createProject(UserModel owner, String name) { + ProjectModel project = new ProjectModel() + .withName(name) + .withOwner(owner); + + project.setId(UUID.randomUUID()); + project.setPath(defaultPath + project.getId().toString()); + + database.put(project.getId(), project); + + return project; + } + + @Override + public List getAllProjects() { + return database.values().stream().toList(); + } + + @Override + public Optional getProject(UUID id) { + return Optional.ofNullable(database.get(id)); + } + + @Override + public List getAllOwnedProjects(UserModel owner) { + return database + .values() + .stream() + .filter( + project -> project + .getOwner() + .getId() + .compareTo(owner.getId()) == 0 + ).toList(); + } + + @Override + public void updateName(UUID id, String name) { + ProjectModel project = database.get(id); + + if (project == null) { + return; + } + + project.setName(name); + database.put(id, project); + } + + @Override + public void updateOwner(UUID id, UserModel newOwner) { + ProjectModel project = database.get(id); + + if (project == null) { + return; + } + + project.setOwner(newOwner); + database.put(id, project); + } + + @Override + public void addMember(UUID id, UserModel member) { + ProjectModel project = database.get(id); + + if (project == null) { + return; + } + + project.getMembers().add(member); + database.put(id, project); + } + + @Override + public void removeMember(UUID id, UserModel member) { + ProjectModel project = database.get(id); + + if (project == null) { + return; + } + + project.getMembers().remove(member); + database.put(id, project); + } + + @Override + public void deleteProject(UUID id) { + database.remove(id); + } +} diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/UserFakeRepository.java b/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/UserFakeRepository.java new file mode 100644 index 0000000..bd4861a --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/data/repositories/UserFakeRepository.java @@ -0,0 +1,121 @@ +package fr.epita.assistants.ping.data.repositories; + +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repository.UserRepository; + +import java.util.*; + + +public class UserFakeRepository extends UserRepository { + + private final Map database = new HashMap<>(); + + @Override + public long count() { + return database.size(); + } + + public UserModel createUser(UserModel user) { + database.put(user.getId(), user); + + return user; + } + + @Override + public UserModel createUser(String name, Boolean admin, String avatar, String login, String password) { + UserModel user = new UserModel() + .withAvatar(avatar) + .withLogin(login) + .withPassword(password) + .withIsAdmin(admin) + .withDisplayName(name); + + user.setId(UUID.randomUUID()); + + return createUser(user); + } + + @Override + public List getAllUsers() { + return database.values().stream().toList(); + } + + @Override + public Optional getUser(UUID id) { + return Optional.ofNullable(database.get(id)); + } + + @Override + public Optional getUser(String login) { + return database + .values() + .stream() + .filter(userModel -> Objects.equals(userModel.getLogin(), login)) + .findFirst(); + } + + @Override + public void updateLogin(UUID id, String login) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setLogin(login); + database.put(id, user); + } + + @Override + public void updatePassword(UUID id, String password) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setPassword(password); + database.put(id, user); + } + + @Override + public void updateIsAdmin(UUID id, Boolean admin) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setIsAdmin(admin); + database.put(id, user); + } + + @Override + public void updateAvatar(UUID id, String avatar) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setAvatar(avatar); + database.put(id, user); + } + + @Override + public void updateDisplayName(UUID id, String name) { + UserModel user = database.get(id); + + if (user == null) { + return; + } + + user.setDisplayName(name); + database.put(id, user); + } + + @Override + public void deleteUser(UUID id) { + database.remove(id); + } +} diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestHelloWorldResource.java b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestHelloWorldResource.java new file mode 100644 index 0000000..1368cf3 --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestHelloWorldResource.java @@ -0,0 +1,31 @@ +package fr.epita.assistants.ping.presentation.rest; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +@QuarkusTest +@TestHTTPEndpoint(HelloWorldResource.class) +public class TestHelloWorldResource { + + @Test + public void testHelloWorldEndpoint() { + given() + .when().get("/hello") + .then() + .statusCode(200) + .body(equalTo("Hello World !")); + } + + @Test + public void testErrorEndpoint() { + given() + .when().get("/error") + .then() + .statusCode(400) + .body("message", equalTo("Example error: This is an error")); + } +} diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestProjectResource.java b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestProjectResource.java new file mode 100644 index 0000000..54bf30c --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestProjectResource.java @@ -0,0 +1,32 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.data.repository.UserRepository; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; + +@QuarkusTest +public class TestProjectResource { + + @BeforeAll + public static void setup() + { + // TODO : Create the two users, store their JWT + } + + @Nested + class GetMyProject { + + @Test + public void GetNonExistent() + { + given().when().get("/api/hello").then().statusCode(200); + } + + } + +} diff --git a/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestUserResource.java b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestUserResource.java new file mode 100644 index 0000000..1d08df6 --- /dev/null +++ b/ping/ping/src/test/java/fr/epita/assistants/ping/presentation/rest/TestUserResource.java @@ -0,0 +1,431 @@ +package fr.epita.assistants.ping.presentation.rest; + +import fr.epita.assistants.ping.api.responses.UserResponse; +import fr.epita.assistants.ping.data.model.UserModel; +import fr.epita.assistants.ping.data.repositories.UserFakeRepository; +import fr.epita.assistants.ping.data.repository.UserRepository; +import io.quarkus.test.junit.QuarkusMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.quarkus.test.security.jwt.Claim; +import io.quarkus.test.security.jwt.JwtSecurity; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.junit.jupiter.api.*; + +import java.util.UUID; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +public class TestUserResource { + + UserFakeRepository userFakeRepository; + + private static final String testUserUUIDStr = "5ed796ba-e0b9-476d-b4e0-6924ae1144be"; + private static final UUID testUserUUID = UUID.fromString(testUserUUIDStr); + private static final String testUserUsername = "test_user"; + private static final String testUserPassword = "bombardillo"; + private static final UserModel testUser = new UserModel() + .withLogin(testUserUsername) + .withPassword(testUserPassword) + .withDisplayName(testUserUsername) + .withAvatar(null) + .withIsAdmin(false); + + private static final String testAdminUUIDStr = "d962b6b0-79fb-40a7-a12d-fdbb416aa7b2"; + private static final UUID testAdminUUID = UUID.fromString(testAdminUUIDStr); + private static final String testAdminUsername = "test_admin"; + private static final String testAdminPassword = "crocodillo"; + private static final UserModel testAdmin = new UserModel() + .withLogin(testAdminUsername) + .withPassword(testAdminPassword) + .withDisplayName(testAdminUsername) + .withAvatar(null) + .withIsAdmin(true); + + @BeforeEach + void setupEach() { + userFakeRepository = new UserFakeRepository(); + + testUser.setId(testUserUUID); + testAdmin.setId(testAdminUUID); + + userFakeRepository.createUser(testUser); + userFakeRepository.createUser(testAdmin); + + QuarkusMock.installMockForType(userFakeRepository, UserRepository.class); + } + + @Nested + class TestSession { + + @Nested + class TestLogin { + + @Test + public void testLoginGood() { + final String testPayload = "{" + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": \"" + testUserPassword + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(200) + .body("token", anything()); + } + + @Test + public void testLoginBadPassword() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": \"" + testUserPassword + "_123\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(401) + .body("message", containsString("Password is incorrect")); + } + + @Test + public void testLoginBadUsername() { + final String testPayload = "{" + + "\"login\": \"" + testUserUsername + "_123\", " + + "\"password\": \"" + testUserPassword + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(401) + .body("message", containsString("User does not exist")); + } + + @Test + public void testLoginNonexistant() { + final String testPayload = "{ " + + "\"login\": \"" + "bad_user" + "\", " + + "\"password\": \"" + "wiwiwi" + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(401) + .body("message", containsString("User does not exist")); + } + + @Test + public void testLoginInvalidUsername() { + final String testPayload = "{ " + + "\"login\": \"\", " + + "\"password\": \"" + "wiwiwi" + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(400) + .body("message", containsString("Username field is empty or null")); + } + + @Test + public void testLoginInvalidPassword() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": \"\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(400) + .body("message", containsString("Password field is empty or null")); + } + + @Test + public void testLoginNullUsername() { + final String testPayload = "{ " + + "\"login\": null, " + + "\"password\": \"" + testUserPassword + "\" " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(400) + .body("message", containsString("Username field is empty or null")); + } + + @Test + public void testLoginNullPassword() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": null " + + "} "; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .when().post("/api/user/login") + .then() + .statusCode(400) + .body("message", containsString("Password field is empty or null")); + } + } + + @Nested + class TestRefresh { + + @Test + @TestSecurity(user = testUserUsername, roles = { "user" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testUserUUIDStr) + } + ) + public void testRefreshGood() { + given() + .when().get("/api/user/refresh") + .then() + .statusCode(200) + .body("token", anything()); + } + + @Test + @TestSecurity(user = testUserUsername, roles = { "user" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = "32516464-07c7-4aad-9585-d248e45dbdf8") + } + ) + public void testRefreshBad() { + given() + .when().get("/api/user/refresh") + .then() + .statusCode(401) + .body("message", containsString( + "Session JWT lead to an user who does not exist." + + " You may need to login again.") + ); + } + + @Test + public void testRefreshNotLogged() { + given() + .when().get("/api/user/refresh") + .then() + .statusCode(401) + .body(emptyString()); + } + } + } + + @Nested + class TestUser { + + @Nested + class TestAllUsers { + + @Test + @TestSecurity(user = testAdminUsername, roles = { "admin" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testAdminUUIDStr) + } + ) + public void testAllUsersAuthorized() { + given() + .when().get("/api/user/all") + .then() + .statusCode(200) + .extract() + .as(UserResponse[].class); + } + + @Test + @TestSecurity(user = testUserUsername, roles = { "user" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testUserUUIDStr) + } + ) + public void testAllUsersUnauthorized() { + given() + .when().get("/api/user/all") + .then() + .statusCode(403) + .body(emptyString()); + } + + @Test + public void testAllUsersNotConnected() { + given() + .when().get("/api/user/all") + .then() + .statusCode(401) + .body(emptyString()); + } + } + + @Nested + class TestCreateUser { + + private Response givenTemplate(final String body) { + return given() + .contentType(ContentType.JSON) + .body(body) + .when().post("/api/user"); + } + + @Nested + @TestSecurity(user = testAdminUsername, roles = { "admin" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testAdminUUIDStr) + } + ) + class TestCreateUserAuthenticated { + + @Test + public void testCreateUserValid() { + final String newUsername = "jammy.oiaia"; + final String newPassword = "fred"; + final String testPayload = "{ " + + "\"login\": \"" + newUsername + "\", " + + "\"password\": \"" + newPassword + "\", " + + "\"isAdmin\": \"false\"" + + "}"; + + final long oldCount = userFakeRepository.count(); + + givenTemplate(testPayload) + .then() + .statusCode(200) + .body("id", matchesPattern("^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}$")) + .body("login", equalTo(newUsername)) + .body("displayName", equalTo("Jammy Oiaia")) + .body("isAdmin", equalTo(false)) + .body("avatar", equalTo(null)); + + Assertions.assertEquals(userFakeRepository.count(), oldCount + 1); + } + + @Test + public void testCreateUserInvalid() { + final String newUsername = "jammy"; + final String newPassword = "fred"; + final String testPayload = "{ " + + "\"login\": \"" + newUsername + "\", " + + "\"password\": \"" + newPassword + "\", " + + "\"isAdmin\": \"false\"" + + "}"; + + givenTemplate(testPayload) + .then() + .statusCode(400) + .body("message", containsString("Login must only have at most a dot or a dash")); + } + + @Test + public void testCreateUserLoginExistant() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "\", " + + "\"password\": \"" + testUserPassword + "\", " + + "\"isAdmin\": false" + + " }"; + + givenTemplate(testPayload) + .then() + .statusCode(409) + .body("message", containsString("User with login " + testUserUsername + " already exists")); + } + + @Test + public void testCreateUserNull() { + final String newPassword = "fred"; + final String testPayload = "{ " + + "\"login\": null, " + + "\"password\": \"" + newPassword + "\", " + + "\"isAdmin\": \"false\"" + + "}"; + + givenTemplate(testPayload) + .then() + .statusCode(400) + .body("message", containsString("Username should not be empty or null")); + } + + @Test + public void testCreatePasswordNull() { + final String newLogin = "fred.pp"; + final String testPayload = "{ " + + "\"login\": \"" + newLogin + "\", " + + "\"password\": null, " + + "\"isAdmin\": \"false\"" + + "}"; + + givenTemplate(testPayload) + .then() + .statusCode(400) + .body("message", containsString("Password should not be empty or null")); + } + } + + @Test + @TestSecurity(user = testUserUsername, roles = { "user" }) + @JwtSecurity( + claims = { + @Claim(key = "sub", value = testUserUUIDStr) + } + ) + public void testCreateUserNotAnAdmin() { + final String testPayload = "{ " + + "\"login\": \"" + testUserUsername + "_2\", " + + "\"password\": \"" + testUserPassword + "\", " + + "\"isAdmin\": false\"\"" + + "}"; + + given() + .contentType(ContentType.JSON) + .body(testPayload) + .then() + .statusCode(403) + .body(emptyString()); + } + + @Test + public void testCreateUserNotConnected() { + final String testPayload = "{ " + + "\"login\": " + testUserUsername + "_2\", " + + "\"password\": \"" + testUserPassword + "\", " + + "\"isAdmin\": \"false\"" + + "}"; + + givenTemplate(testPayload) + .then() + .statusCode(401) + .body(emptyString()); + } + } + } +} diff --git a/ping/ping/src/test/resources/application.properties b/ping/ping/src/test/resources/application.properties new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3