commit 3cd59337e7333ba8094b786de4a1ceebf5436b3e Author: Chuck1sn Date: Wed May 14 10:16:48 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..887efcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,171 @@ +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +.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 + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# 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 + +# 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* + + +# 如何在这个地方引用 backend 和 frontend 下级目录中的 .gitignore 中的规则?我需要全部给他们前面加上 backend/ 和 frontend/ 前缀吗? diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..7572a47 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Chuck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd3f8fc --- /dev/null +++ b/README.md @@ -0,0 +1,133 @@ +# 知路管理后台 + +一个重新构思、重新设计、重新开发的现代化 Java 前后端脚手架。整合大量高效现代化技术栈,全网唯一。 + +- [知路管理后台](#%E7%9F%A5%E8%B7%AF%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0) + - [尊重设计与编码规范](#%E5%B0%8A%E9%87%8D%E8%AE%BE%E8%AE%A1%E4%B8%8E%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83) + - [正确的业务设计与建模](#%E6%AD%A3%E7%A1%AE%E7%9A%84%E4%B8%9A%E5%8A%A1%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%BB%BA%E6%A8%A1) + - [专属业务功能](#%E4%B8%93%E5%B1%9E%E4%B8%9A%E5%8A%A1%E5%8A%9F%E8%83%BD) + - [云原生开发与秒级部署](#%E4%BA%91%E5%8E%9F%E7%94%9F%E5%BC%80%E5%8F%91%E4%B8%8E%E7%A7%92%E7%BA%A7%E9%83%A8%E7%BD%B2) + - [自动免费的 HTTPS 证书](#%E8%87%AA%E5%8A%A8%E5%85%8D%E8%B4%B9%E7%9A%84-https-%E8%AF%81%E4%B9%A6) + - [自动化数据库管理](#%E8%87%AA%E5%8A%A8%E5%8C%96%E6%95%B0%E6%8D%AE%E5%BA%93%E7%AE%A1%E7%90%86) + - [其他更多特性](#%E5%85%B6%E4%BB%96%E6%9B%B4%E5%A4%9A%E7%89%B9%E6%80%A7) + - [技术选型](#%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B) + +## 产品社群 + +**加 QQ 群,获取一键部署脚本** +QQ群:638254979 +**加微信,获取 VIP 客户支持(喂饭级技术讲解+预购所有课程)** +微信:Chuck9996 +**相关课程** +已上线: +[《重构方法论与单元测试的艺术》](https://www.bilibili.com/cheese/play/ep1615343) +敬请期待:(加群获取) +[《知路脚手架喂饭级教程》]() +[《领域驱动没那么复杂-贫民项目的领域架构实战》]() + +## 尊重设计与编码规范 + +本系统在开发过程中以《TDD测试驱动开发》为指导思想,在业务代码中贯彻落实了严格、规范、优良的编码与设计;并编写了大量的单元测试、集成测试、切片测试为你的应用保驾护航。 + +本系统的测试代码全网独一无二,内容无可挑剔;其中包含大量编码设计的哲学理念。 + +具体内容请参考 test 目录,并辅以视频教程进行阅读,它将会使你受益匪浅。 +[👉《重构方法论与单元测试的艺术》👈](https://www.bilibili.com/cheese/play/ep1615343) +![class](/assets/class.png) + +吃透这款脚手架与配套课程,从今以后就不是别人 Review 你的代码,而是你对别人的代码进行指摘。 + +## 正确的业务设计与建模 + +大部分同类产品,为了规避某些复杂的业务逻辑,强行改变本来的业务形态:如强行把某些多对多关系的业务设计为一对多等等。**这叫做鸵鸟战术:即把头埋进沙子里面假装这件事情不存在。** + +这样的设计不仅没有解决问题,反而增加了问题。一个无法解决问题的管理系统是没有价值的。 + +而知路管理后台从一开始就致力于正确的业务建模——当面对复杂的产品逻辑时,我们不采取鸵鸟战术,而是采取以下两个手段直面困难: + +- 在框架层面进行抽象与封装 +- 使用现代化的技术选型从根本上解决 + +使用本系统构建的系统,只需要几行代码即可轻松编写之前难以实现的复杂业务逻辑,让开发者倍感轻松。 + +### 专属业务功能 + +得益于上述设计思想,在本系统中用户不仅可以担任多个岗位,还能够隶属于多个部门,同时还可拥有多个部门的数据权限。另外,岗位和部门还可以相互配合,提供更加复杂的产品逻辑的实现支持。 + +![depbind](/assets/depbind.png) +![posbind](/assets/posbind.png) + +今后还将推出更多复杂业务逻辑的解决方案,敬请期待。 + +## 云原生开发与零配置部署 + +知路管理后台是完全围绕云原生进行设计开发的。这意味着你只需要三个步骤就可以部署完整个前后端系统: + +1. 拥有一台安装了操作系统的电脑 +2. 安装 Docker +3. 下载代码,一键运行部署脚本 + +你不需要安装 Java,不需要安装 Javascript,不需要安装 Vue,不需要安装 mysql,不需要配置这个,修改那个。 + +无论你是 Linux 还是 Mac 还是 Windows,都能够在 2 分钟内一键部署好整个系统。 + +**获取部署脚本,请加 QQ 群:638254979** + +## 自动免费的 HTTPS 证书 + +本系统会在「开发环境->测试环境->生产环境」自动生成并配置免费的 Https 证书供你使用。不需要任何配置,只要运行部署脚本即可马上获取这项功能。 + +如果你是一个有经验的开发者,尤其是前端开发者,就应该能明白在开发和测试环境使用 Https 调试是多么的重要。 +>注意,开发环境和测试环境的证书是自签名的,这意味着访问网站时需要手动点击信任按钮 + +## 自动化数据库管理 + +本系统会自动管理将所有数据库功能,包括自动建表、自动修改、删除字段、自动增加索引等; +不仅如此,脚手架还会在代码库中对你的修改历史进行版本管理。从而方便你对任意时间点的数据库修改进行回朔。 + +总而言之,你唯一需要做的就是业务编码,然后把其他复杂的事情交给脚手架。 + +## 更多特性 + +- 开发、测试、生产全生态链云原生环境 +- 通过 .env 管理开发、测试、生产环境所有账号密码。 +- 在框架层面集成的代码格式化与规范检查 +- 自动生成数据库建模、DAO、单表 CRUD +- 其他更多功能 + +## 部分技术选型 + +**前端** + +| 框架 | 版本 | +|---|---| +| node | lts/jod | +| vue-family | ^3.5 | +| vite-family | ^6.2.1 | +| tailwindcss | ^4.0 | +| zod | ^3.2 | +| pinia |^3.0 | +| biome |^1.9.4 | +| playwright |^1.51.1 | +| msw |^1.51.1 | +| openapi-typescript | ^7.6.1 | +| typescript | ~5.8.0 | +| docker | ^27 | + +**后端** + +| 技术 | 版本 | +|---|---| +| java | 21 | +| spring-boot | 3.3.9 | +| spring-security | 3.3.9 | +| spring-cache | 3.3.9 | +| spring-doc | 2.6.0 | +| test-containers | 1.20.6 | +| jooq | 3.19.18 | +| postgresql | 17.3 | +| flyway | 11.4 | +| spotless | 7.0.2 | +| pmd | 7.9.0 | +| gradle | 8.13 | +| docker | ^27 | diff --git a/assets/class.png b/assets/class.png new file mode 100644 index 0000000..313acf5 Binary files /dev/null and b/assets/class.png differ diff --git a/assets/depbind.png b/assets/depbind.png new file mode 100644 index 0000000..33821c9 Binary files /dev/null and b/assets/depbind.png differ diff --git a/assets/posbind.png b/assets/posbind.png new file mode 100644 index 0000000..235fb49 Binary files /dev/null and b/assets/posbind.png differ diff --git a/assets/qq.jpg b/assets/qq.jpg new file mode 100644 index 0000000..885ea6d Binary files /dev/null and b/assets/qq.jpg differ diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..16a473b --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,217 @@ +### Java template +# 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 template +.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 + +### JetBrains template +# 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 + +### dotenv template +.env + +### Git template +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +.git +.gitignore +README.md diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..6bbb5b2 --- /dev/null +++ b/backend/.env @@ -0,0 +1,17 @@ +COMPOSE_PROJECT_NAME=mjga +# database +DATABASE_HOST_PORT=host.docker.internal:5432 +DATABASE_STORE=~/docker/store/mjga/db/data +DATABASE_DB=mjga +DATABASE_DEFAULT_SCHEMA=public +DATABASE_USER=mjga +DATABASE_PASSWORD=mjga +DATABASE_EXPOSE_PORT=5432 +# web +WEB_EXPOSE_PORT=8080 +LOG_PATH=~/docker/store/mjga/log +# cors +ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:8080,http://localhost:5173 +ALLOWED_METHODS=* +ALLOWED_HEADERS=* +ALLOWED_EXPOSE_HEADERS=* diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..7ff7906 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,229 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +assets + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +### Java template +# 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 template +.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 + +### JetBrains template +# 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 + +# 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 + +### Gradle template +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +### Windows template +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + + +logs +db.d/store/* +.git + + diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts new file mode 100644 index 0000000..2e6aae5 --- /dev/null +++ b/backend/build.gradle.kts @@ -0,0 +1,194 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +val jooqVersion by extra("3.19.22") +val testcontainersVersion by extra("1.20.6") +val flywayVersion by extra("11.4.0") + +plugins { + java + `java-library` + jacoco + id("org.springframework.boot") version "3.3.9" + id("io.spring.dependency-management") version "1.1.7" + id("org.springdoc.openapi-gradle-plugin") version "1.9.0" + id("pmd") + id("org.jooq.jooq-codegen-gradle") version "3.19.22" + id("com.diffplug.spotless") version "7.0.2" +} + +sourceSets { + main { + java { + srcDir("build/generated-sources/jooq") + } + } + test { + java { + srcDir("build/generated-sources/jooq") + } + } +} + +group = "com.zl.mjga" +version = "1.0.0" +description = "make java great again!" +java.sourceCompatibility = JavaVersion.VERSION_17 + +configurations { + compileOnly { + extendsFrom(configurations.annotationProcessor.get()) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-jooq") + implementation("org.springframework.boot:spring-boot-starter-mail") + implementation("org.springframework.boot:spring-boot-starter-cache") + implementation("org.springframework.boot:spring-boot-starter-quartz") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-aop") + implementation("org.apache.commons:commons-lang3:3.17.0") + implementation("org.apache.commons:commons-collections4:4.4") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") + implementation("org.jooq:jooq-meta:$jooqVersion") + implementation("com.auth0:java-jwt:4.4.0") + implementation("org.flywaydb:flyway-core:$flywayVersion") + implementation("org.flywaydb:flyway-database-postgresql:$flywayVersion") + implementation("com.github.ben-manes.caffeine:caffeine:3.2.0") + implementation("org.springframework.boot:spring-boot-starter-quartz") + testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion") + testImplementation("org.testcontainers:postgresql:$testcontainersVersion") + testImplementation("org.testcontainers:testcontainers-bom:$testcontainersVersion") + runtimeOnly("org.postgresql:postgresql") + compileOnly("org.projectlombok:lombok") + developmentOnly("org.springframework.boot:spring-boot-devtools") + testImplementation("org.springframework.boot:spring-boot-testcontainers") + testImplementation("org.springframework.boot:spring-boot-starter-webflux") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + jooqCodegen("org.postgresql:postgresql") + jooqCodegen("org.jooq:jooq-codegen:$jooqVersion") + jooqCodegen("org.jooq:jooq-meta-extensions:$jooqVersion") + annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") + annotationProcessor("org.projectlombok:lombok") + api("org.jspecify:jspecify:1.0.0") +} + +tasks.withType { + archiveFileName.set("dbfirst.jar") +} + +tasks.withType { + useJUnitPlatform() +} + +tasks.test { + finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) // tests are required to run before generating the report +} + +jacoco { + toolVersion = "0.8.12" + reportsDirectory.set(layout.buildDirectory.dir("reports/jacoco")) +} + +pmd { + sourceSets = listOf(java.sourceSets.findByName("main")) + isConsoleOutput = true + toolVersion = "7.9.0" + rulesMinimumPriority.set(5) + ruleSetFiles = files("pmd-rules.xml") +} + +spotless { + format("misc") { + // define the files to apply `misc` to + target("*.gradle.kts", "*.md", ".gitignore") + // define the steps to apply to those files + trimTrailingWhitespace() + leadingTabsToSpaces() + endWithNewline() + } + + java { + googleJavaFormat("1.25.2").reflowLongStrings() + formatAnnotations() + } + + kotlinGradle { + target("*.gradle.kts") // default target for kotlinGradle + ktlint() // or ktfmt() or prettier() + } +} + +jooq { + configuration { + generator { + database { + includes = ".*" +// excludes = "qrtz_.*" + name = "org.jooq.meta.extensions.ddl.DDLDatabase" + properties { + property { + key = "scripts" + value = "src/main/resources/db/migration/*.sql" + } + property { + key = "sort" + value = "semantic" + } + property { + key = "unqualifiedSchema" + value = "none" + } + property { + key = "defaultNameCase" + value = "lower" + } + property { + key = "logExecutedQueries" + value = "true" + } + property { + key = "logExecutionResults" + value = "true" + } + } + forcedTypes { + forcedType { + name = "varchar" + includeExpression = ".*" + includeTypes = "JSONB?" + } + forcedType { + name = "varchar" + includeExpression = ".*" + includeTypes = "INET" + } + } + } + generate { + isDaos = true + isRecords = true + isDeprecated = false + isImmutablePojos = false + isFluentSetters = true + isSpringAnnotations = true + isSpringDao = true + } + target { + packageName = "org.jooq.generated" + } + } + } +} diff --git a/backend/gradle.properties b/backend/gradle.properties new file mode 100644 index 0000000..1063152 --- /dev/null +++ b/backend/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.configuration-cache=false +org.gradle.parallel=true +org.gradle.caching=true diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/backend/gradle/wrapper/gradle-wrapper.jar differ diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f853b --- /dev/null +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/gradlew b/backend/gradlew new file mode 100755 index 0000000..f3b75f3 --- /dev/null +++ b/backend/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/pmd-rules.xml b/backend/pmd-rules.xml new file mode 100644 index 0000000..4fe6859 --- /dev/null +++ b/backend/pmd-rules.xml @@ -0,0 +1,17 @@ + + + + This is a custom ruleset for our project. + + + + + + + + + diff --git a/backend/settings.gradle.kts b/backend/settings.gradle.kts new file mode 100644 index 0000000..532f3be --- /dev/null +++ b/backend/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "dbfirst" diff --git a/backend/src/main/java/com/zl/mjga/ApplicationService.java b/backend/src/main/java/com/zl/mjga/ApplicationService.java new file mode 100644 index 0000000..4c28dd7 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/ApplicationService.java @@ -0,0 +1,12 @@ +package com.zl.mjga; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = {"com.zl.mjga", "org.jooq.generated"}) +public class ApplicationService { + + public static void main(String[] args) { + SpringApplication.run(ApplicationService.class, args); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/CorsConfig.java b/backend/src/main/java/com/zl/mjga/config/CorsConfig.java new file mode 100644 index 0000000..8c795a7 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/CorsConfig.java @@ -0,0 +1,45 @@ +package com.zl.mjga.config; + +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Value("${cors.allowedOrigins}") + private String allowedOrigins; + + @Value("${cors.allowedMethods}") + private String allowedMethods; + + @Value("${cors.allowedHeaders}") + private String allowedHeaders; + + @Value("${cors.allowedExposeHeaders}") + private String allowedExposeHeaders; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**"); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(","))); + configuration.setAllowedMethods(Arrays.asList(allowedMethods.split(","))); + configuration.setAllowedHeaders(Arrays.asList(allowedHeaders.split(","))); + configuration.setExposedHeaders(Arrays.asList(allowedExposeHeaders.split(","))); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/QuartzConfig.java b/backend/src/main/java/com/zl/mjga/config/QuartzConfig.java new file mode 100644 index 0000000..de304d5 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/QuartzConfig.java @@ -0,0 +1,79 @@ +package com.zl.mjga.config; + +import com.zl.mjga.job.DataBackupJob; +import java.util.Map; +import java.util.Properties; +import javax.sql.DataSource; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.Trigger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.quartz.CronTriggerFactoryBean; +import org.springframework.scheduling.quartz.JobDetailFactoryBean; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +@Configuration +public class QuartzConfig { + + @Value("${spring.flyway.default-schema}") + private String defaultSchema; + + @Bean("emailJobSchedulerFactory") + public SchedulerFactoryBean emailJobSchedulerFactory(DataSource dataSource) { + SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean(); + schedulerFactory.setSchedulerName("email-scheduler"); + schedulerFactory.setDataSource(dataSource); + Properties props = getCommonProps(); + props.setProperty("org.quartz.threadPool.threadCount", "10"); + schedulerFactory.setQuartzProperties(props); + return schedulerFactory; + } + + @Bean("dataBackupJobDetail") + public JobDetailFactoryBean dataBackupJobDetail() { + JobDetailFactoryBean factory = new JobDetailFactoryBean(); + factory.setJobClass(DataBackupJob.class); + factory.setJobDataMap(new JobDataMap(Map.of("roleId", "Gh2mxa"))); + factory.setName("data-backup-job"); + factory.setGroup("batch-service"); + factory.setDurability(true); + return factory; + } + + @Bean("dataBackupSchedulerFactory") + public SchedulerFactoryBean dataBackupSchedulerFactory( + Trigger dataBackupTrigger, JobDetail dataBackupJobDetail, DataSource dataSource) { + SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean(); + schedulerFactory.setSchedulerName("data-backup-scheduler"); + Properties props = getCommonProps(); + props.setProperty("org.quartz.threadPool.threadCount", "10"); + schedulerFactory.setQuartzProperties(props); + schedulerFactory.setJobDetails(dataBackupJobDetail); + schedulerFactory.setTriggers(dataBackupTrigger); + schedulerFactory.setDataSource(dataSource); + + return schedulerFactory; + } + + private Properties getCommonProps() { + Properties props = new Properties(); + props.setProperty( + "org.quartz.jobStore.class", + "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); + props.setProperty( + "org.quartz.jobStore.driverDelegateClass", + "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"); + props.setProperty("org.quartz.jobStore.tablePrefix", String.format("%s.qrtz_", defaultSchema)); + return props; + } + + @Bean("dataBackupTrigger") + public CronTriggerFactoryBean dataBackupTrigger(JobDetail dataBackupJobDetail) { + CronTriggerFactoryBean factory = new CronTriggerFactoryBean(); + factory.setJobDetail(dataBackupJobDetail); + factory.setCronExpression("0 0/5 * * * ?"); + return factory; + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/cache/CacheConfig.java b/backend/src/main/java/com/zl/mjga/config/cache/CacheConfig.java new file mode 100644 index 0000000..0de18c1 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/cache/CacheConfig.java @@ -0,0 +1,31 @@ +package com.zl.mjga.config.cache; + +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@EnableCaching +@Configuration +public class CacheConfig { + + public static final String VERIFY_CODE = "verifyCode"; + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(List.of(verifyCodeCache())); + return cacheManager; + } + + private CaffeineCache verifyCodeCache() { + return new CaffeineCache( + VERIFY_CODE, + Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(60, TimeUnit.SECONDS).build()); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/HttpFireWallConfig.java b/backend/src/main/java/com/zl/mjga/config/security/HttpFireWallConfig.java new file mode 100644 index 0000000..2f98d9e --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/security/HttpFireWallConfig.java @@ -0,0 +1,30 @@ +package com.zl.mjga.config.security; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.RequestRejectedHandler; +import org.springframework.security.web.firewall.StrictHttpFirewall; + +@Configuration +public class HttpFireWallConfig { + + @Bean + public HttpFirewall getHttpFirewall() { + return new StrictHttpFirewall(); + } + + @Bean + public RequestRejectedHandler requestRejectedHandler() { + return (request, response, requestRejectedException) -> { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setContentType(MediaType.TEXT_PLAIN_VALUE); + try (PrintWriter writer = response.getWriter()) { + writer.write(requestRejectedException.getMessage()); + } + }; + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/Jwt.java b/backend/src/main/java/com/zl/mjga/config/security/Jwt.java new file mode 100644 index 0000000..59e804e --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/security/Jwt.java @@ -0,0 +1,79 @@ +package com.zl.mjga.config.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@Getter +public class Jwt { + + private final String secret; + + private final int expirationMin; + + private final JWTVerifier verifier; + + public Jwt( + @Value("${jwt.secret}") String secret, @Value("${jwt.expiration-min}") int expirationMin) { + this.verifier = JWT.require(Algorithm.HMAC256(secret)).build(); + this.secret = secret; + this.expirationMin = expirationMin; + } + + public String getSubject(String token) { + return JWT.decode(token).getSubject(); + } + + public Boolean verify(String token) { + try { + verifier.verify(token); + return Boolean.TRUE; + } catch (JWTVerificationException e) { + return Boolean.FALSE; + } + } + + public String extract(HttpServletRequest request) { + String authorization = request.getHeader("Authorization"); + if (StringUtils.isNotEmpty(authorization) && authorization.startsWith("Bearer")) { + return authorization.substring(7); + } else { + return null; + } + } + + public String create(String userIdentify) { + return JWT.create() + .withSubject(String.valueOf(userIdentify)) + .withIssuedAt(new Date()) + .withExpiresAt( + Date.from( + LocalDateTime.now() + .plusMinutes(expirationMin) + .atZone(ZoneId.systemDefault()) + .toInstant())) + .sign(Algorithm.HMAC256(secret)); + } + + public void makeToken( + HttpServletRequest request, HttpServletResponse response, String userIdentify) { + response.addHeader("Authorization", String.format("Bearer %s", create(userIdentify))); + } + + public void removeToken(HttpServletRequest request, HttpServletResponse response) { + response.addHeader("Authorization", null); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/JwtAuthenticationFilter.java b/backend/src/main/java/com/zl/mjga/config/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..14290f5 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/security/JwtAuthenticationFilter.java @@ -0,0 +1,44 @@ +package com.zl.mjga.config.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +@Setter +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final Jwt jwt; + + private final UserDetailsServiceImpl userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String token = jwt.extract(request); + if (StringUtils.isNotEmpty(token) && jwt.verify(token)) { + try { + UserDetails userDetails = userDetailsService.loadUserByUsername(jwt.getSubject(token)); + JwtAuthenticationToken authenticated = + JwtAuthenticationToken.authenticated(userDetails, token, userDetails.getAuthorities()); + authenticated.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authenticated); + } catch (Exception e) { + log.error("jwt with invalid user id {}", jwt.getSubject(token), e); + } + } + filterChain.doFilter(request, response); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/JwtAuthenticationToken.java b/backend/src/main/java/com/zl/mjga/config/security/JwtAuthenticationToken.java new file mode 100644 index 0000000..35e1668 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/security/JwtAuthenticationToken.java @@ -0,0 +1,57 @@ +package com.zl.mjga.config.security; + +import java.io.Serial; +import java.util.Collection; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.userdetails.UserDetails; + +@Setter +@Getter +@ToString +public class JwtAuthenticationToken extends AbstractAuthenticationToken { + + @Serial private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private final Object principal; + + private String credentials; + + public JwtAuthenticationToken(Object principal, String credentials) { + super(null); + this.principal = principal; + this.credentials = credentials; + super.setAuthenticated(false); + } + + public JwtAuthenticationToken( + Object principal, String credentials, Collection authorities) { + super(authorities); + this.principal = principal; + this.credentials = credentials; + super.setAuthenticated(true); + } + + public static JwtAuthenticationToken unauthenticated(String userIdentify, String token) { + return new JwtAuthenticationToken(userIdentify, token); + } + + public static JwtAuthenticationToken authenticated( + UserDetails principal, String token, Collection authorities) { + return new JwtAuthenticationToken(principal, token, authorities); + } + + @Override + public String getCredentials() { + return this.credentials; + } + + @Override + public Object getPrincipal() { + return this.principal; + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/PasswordEncoderConfig.java b/backend/src/main/java/com/zl/mjga/config/security/PasswordEncoderConfig.java new file mode 100644 index 0000000..da6ef3d --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/security/PasswordEncoderConfig.java @@ -0,0 +1,14 @@ +package com.zl.mjga.config.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfig { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/RestfulAuthenticationEntryPointHandler.java b/backend/src/main/java/com/zl/mjga/config/security/RestfulAuthenticationEntryPointHandler.java new file mode 100644 index 0000000..e2584a1 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/security/RestfulAuthenticationEntryPointHandler.java @@ -0,0 +1,28 @@ +package com.zl.mjga.config.security; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; + +public class RestfulAuthenticationEntryPointHandler + implements AccessDeniedHandler, AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/UserDetailsServiceImpl.java b/backend/src/main/java/com/zl/mjga/config/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..9a2b24b --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/security/UserDetailsServiceImpl.java @@ -0,0 +1,38 @@ +package com.zl.mjga.config.security; + +import com.zl.mjga.dto.urp.UserRolePermissionDto; +import com.zl.mjga.service.IdentityAccessService; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserDetailsServiceImpl implements UserDetailsService { + + private final IdentityAccessService identityAccessService; + + @Override + public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException { + UserRolePermissionDto userRolePermissionDto = + identityAccessService.queryUniqueUserWithRolePermission(Long.valueOf(id)); + if (userRolePermissionDto == null) { + throw new UsernameNotFoundException(String.format("uid %s user not found", id)); + } + return new User( + userRolePermissionDto.getUsername(), + userRolePermissionDto.getPassword(), + userRolePermissionDto.getEnable(), + true, + true, + true, + userRolePermissionDto.getPermissions().stream() + .map((permission) -> new SimpleGrantedAuthority(permission.getCode())) + .collect(Collectors.toSet())); + } +} diff --git a/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java b/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java new file mode 100644 index 0000000..b7f2348 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/config/security/WebSecurityConfig.java @@ -0,0 +1,77 @@ +package com.zl.mjga.config.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.*; +import org.springframework.web.cors.CorsConfigurationSource; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor +public class WebSecurityConfig { + + private final UserDetailsServiceImpl userDetailsService; + + private final Jwt jwt; + + private final CorsConfigurationSource corsConfigurationSource; + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public RequestMatcher publicEndPointMatcher() { + return new OrRequestMatcher( + new AntPathRequestMatcher("/auth/sign-in", HttpMethod.POST.name()), + new AntPathRequestMatcher("/auth/sign-up", HttpMethod.POST.name()), + new AntPathRequestMatcher("/v3/api-docs/**", HttpMethod.GET.name()), + new AntPathRequestMatcher("/swagger-ui/**", HttpMethod.GET.name()), + new AntPathRequestMatcher("/swagger-ui.html", HttpMethod.GET.name()), + new AntPathRequestMatcher("/error")); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + RestfulAuthenticationEntryPointHandler restfulAuthenticationEntryPointHandler = + new RestfulAuthenticationEntryPointHandler(); + /* + + http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) + */ + http.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource)); + http.csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + authorize -> + authorize + .requestMatchers(publicEndPointMatcher()) + .permitAll() + .anyRequest() + .authenticated()) + .exceptionHandling( + (exceptionHandling) -> + exceptionHandling + .accessDeniedHandler(restfulAuthenticationEntryPointHandler) + .authenticationEntryPoint(restfulAuthenticationEntryPointHandler)) + .sessionManagement( + session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterAt( + new JwtAuthenticationFilter(jwt, userDetailsService), + UsernamePasswordAuthenticationFilter.class); + return http.build(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/controller/DepartmentController.java b/backend/src/main/java/com/zl/mjga/controller/DepartmentController.java new file mode 100644 index 0000000..3240305 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/controller/DepartmentController.java @@ -0,0 +1,51 @@ +package com.zl.mjga.controller; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.department.DepartmentQueryDto; +import com.zl.mjga.dto.department.DepartmentRespDto; +import com.zl.mjga.repository.DepartmentRepository; +import com.zl.mjga.service.DepartmentService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.jooq.generated.mjga.tables.pojos.Department; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +@RestController +@RequestMapping("/department") +@RequiredArgsConstructor +public class DepartmentController { + + private final DepartmentService departmentService; + private final DepartmentRepository departmentRepository; + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_DEPARTMENT_PERMISSION)") + @GetMapping("/page-query") + @ResponseStatus(HttpStatus.OK) + PageResponseDto> pageQueryDepartments( + @ModelAttribute PageRequestDto pageRequestDto, + @ModelAttribute DepartmentQueryDto departmentQueryDto) { + return departmentService.pageQueryDepartment(pageRequestDto, departmentQueryDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_DEPARTMENT_PERMISSION)") + @GetMapping("/query") + List queryDepartments() { + return departmentRepository.findAll(); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)") + @DeleteMapping() + void deleteDepartment(@RequestParam Long id) { + departmentRepository.deleteById(id); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)") + @PostMapping() + void upsertDepartment(@RequestBody Department department) { + departmentRepository.merge(department); + } +} diff --git a/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java b/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java new file mode 100644 index 0000000..1df2d50 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/controller/IdentityAccessController.java @@ -0,0 +1,177 @@ +package com.zl.mjga.controller; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.department.DepartmentBindDto; +import com.zl.mjga.dto.permission.PermissionBindDto; +import com.zl.mjga.dto.position.PositionBindDto; +import com.zl.mjga.dto.role.RoleBindDto; +import com.zl.mjga.dto.urp.*; +import com.zl.mjga.repository.RoleRepository; +import com.zl.mjga.repository.UserRepository; +import com.zl.mjga.service.IdentityAccessService; +import jakarta.validation.Valid; +import java.security.Principal; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.jooq.generated.mjga.tables.pojos.User; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +@RestController +@RequestMapping("/iam") +@RequiredArgsConstructor +public class IdentityAccessController { + + private final IdentityAccessService identityAccessService; + private final UserRepository userRepository; + private final RoleRepository roleRepository; + + @GetMapping("/me") + UserRolePermissionDto currentUser(Principal principal) { + String name = principal.getName(); + User user = userRepository.fetchOneByUsername(name); + return identityAccessService.queryUniqueUserWithRolePermission(user.getId()); + } + + @PostMapping("/me") + void upsertMe(Principal principal, @RequestBody UserUpsertDto userUpsertDto) { + String name = principal.getName(); + User user = userRepository.fetchOneByUsername(name); + userUpsertDto.setId(user.getId()); + identityAccessService.upsertUser(userUpsertDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @PostMapping("/user") + void upsertUser(@RequestBody UserUpsertDto userUpsertDto) { + identityAccessService.upsertUser(userUpsertDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)") + @GetMapping("/user") + UserRolePermissionDto queryUserWithRolePermission(@RequestParam Long userId) { + return identityAccessService.queryUniqueUserWithRolePermission(userId); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @DeleteMapping("/user") + void deleteUser(@RequestParam Long userId) { + userRepository.deleteById(userId); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @PostMapping("/role") + void upsertRole(@RequestBody RoleUpsertDto roleUpsertDto) { + identityAccessService.upsertRole(roleUpsertDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @DeleteMapping("/role") + void deleteRole(@RequestParam Long roleId) { + roleRepository.deleteById(roleId); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @GetMapping("/role") + RoleDto queryRoleWithPermission(@RequestParam Long roleId) { + return identityAccessService.queryUniqueRoleWithPermission(roleId); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @PostMapping("/permission") + void upsertPermission(@RequestBody PermissionUpsertDto permissionUpsertDto) { + identityAccessService.upsertPermission(permissionUpsertDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @DeleteMapping("/permission") + void deletePermission(@RequestParam Long permissionId) { + roleRepository.deleteById(permissionId); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)") + @GetMapping("/users") + @ResponseStatus(HttpStatus.OK) + PageResponseDto> queryUsers( + @ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute UserQueryDto userQueryDto) { + return identityAccessService.pageQueryUser(pageRequestDto, userQueryDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)") + @GetMapping("/roles") + @ResponseStatus(HttpStatus.OK) + PageResponseDto> queryRoles( + @ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute RoleQueryDto roleQueryDto) { + return identityAccessService.pageQueryRole(pageRequestDto, roleQueryDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)") + @GetMapping("/permissions") + @ResponseStatus(HttpStatus.OK) + PageResponseDto> queryPermissions( + @ModelAttribute PageRequestDto pageRequestDto, + @ModelAttribute PermissionQueryDto permissionQueryDto) { + return identityAccessService.pageQueryPermission(pageRequestDto, permissionQueryDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @PostMapping("/role/bind") + @ResponseStatus(HttpStatus.OK) + void bindRoleBy(@RequestBody @Valid RoleBindDto roleBindDto) { + identityAccessService.bindRoleToUser(roleBindDto.userId(), roleBindDto.roleIds()); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @PostMapping("/role/unbind") + @ResponseStatus(HttpStatus.OK) + void unBindRoleBy(@RequestBody @Valid RoleBindDto roleBindDto) { + identityAccessService.unBindRoleToUser(roleBindDto.userId(), roleBindDto.roleIds()); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @PostMapping("/permission/bind") + @ResponseStatus(HttpStatus.OK) + void bindPermissionBy(@RequestBody @Valid PermissionBindDto permissionBindDto) { + identityAccessService.bindPermissionBy( + permissionBindDto.roleId(), permissionBindDto.permissionIds()); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)") + @PostMapping("/permission/unbind") + @ResponseStatus(HttpStatus.OK) + void unBindPermissionBy(@RequestBody @Valid PermissionBindDto permissionBindDto) { + identityAccessService.unBindPermissionBy( + permissionBindDto.roleId(), permissionBindDto.permissionIds()); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)") + @PostMapping("/department/bind") + @ResponseStatus(HttpStatus.OK) + public void bindDepartmentBy(@RequestBody @Valid DepartmentBindDto departmentBindDto) { + identityAccessService.bindDepartmentBy(departmentBindDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)") + @PostMapping("/department/unbind") + @ResponseStatus(HttpStatus.OK) + public void unBindDepartmentBy(@RequestBody @Valid DepartmentBindDto departmentBindDto) { + identityAccessService.unBindDepartmentBy(departmentBindDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)") + @PostMapping("/position/bind") + @ResponseStatus(HttpStatus.OK) + public void bindPositionBy(@RequestBody @Valid PositionBindDto positionBindDto) { + identityAccessService.bindPositionBy(positionBindDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)") + @PostMapping("/position/unbind") + @ResponseStatus(HttpStatus.OK) + public void unBindPositionBy(@RequestBody @Valid PositionBindDto positionBindDto) { + identityAccessService.unBindPositionBy(positionBindDto); + } +} diff --git a/backend/src/main/java/com/zl/mjga/controller/PositionController.java b/backend/src/main/java/com/zl/mjga/controller/PositionController.java new file mode 100644 index 0000000..5129601 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/controller/PositionController.java @@ -0,0 +1,51 @@ +package com.zl.mjga.controller; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.position.PositionQueryDto; +import com.zl.mjga.dto.position.PositionRespDto; +import com.zl.mjga.repository.PositionRepository; +import com.zl.mjga.service.PositionService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.jooq.generated.mjga.tables.pojos.Position; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +@RestController +@RequestMapping("/position") +@RequiredArgsConstructor +public class PositionController { + + private final PositionService positionService; + private final PositionRepository positionRepository; + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_POSITION_PERMISSION)") + @GetMapping("/page-query") + @ResponseStatus(HttpStatus.OK) + PageResponseDto> pageQueryPositions( + @ModelAttribute PageRequestDto pageRequestDto, + @ModelAttribute PositionQueryDto positionQueryDto) { + return positionService.pageQueryPosition(pageRequestDto, positionQueryDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_POSITION_PERMISSION)") + @GetMapping("/query") + List queryPositions() { + return positionRepository.findAll(); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_POSITION_PERMISSION)") + @DeleteMapping() + void deletePosition(@RequestParam Long id) { + positionRepository.deleteById(id); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_POSITION_PERMISSION)") + @PostMapping() + void upsertPosition(@RequestBody Position position) { + positionRepository.merge(position); + } +} diff --git a/backend/src/main/java/com/zl/mjga/controller/SchedulerController.java b/backend/src/main/java/com/zl/mjga/controller/SchedulerController.java new file mode 100644 index 0000000..63d966a --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/controller/SchedulerController.java @@ -0,0 +1,58 @@ +package com.zl.mjga.controller; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.scheduler.JobKeyDto; +import com.zl.mjga.dto.scheduler.JobTriggerDto; +import com.zl.mjga.dto.scheduler.QueryDto; +import com.zl.mjga.dto.scheduler.TriggerKeyDto; +import com.zl.mjga.service.SchedulerService; +import java.util.Date; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.quartz.JobKey; +import org.quartz.SchedulerException; +import org.quartz.TriggerKey; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/scheduler") +@RequiredArgsConstructor +public class SchedulerController { + + private final SchedulerService schedulerService; + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_SCHEDULER_PERMISSION)") + @GetMapping("/page-query") + public PageResponseDto> pageQuery( + @ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute QueryDto queryDto) { + return schedulerService.getJobWithTriggerBy(pageRequestDto, queryDto); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_SCHEDULER_PERMISSION)") + @PostMapping("/trigger/resume") + public void resumeTrigger(@RequestBody TriggerKeyDto triggerKey) throws SchedulerException { + schedulerService.resumeTrigger(new TriggerKey(triggerKey.name(), triggerKey.group())); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_SCHEDULER_PERMISSION)") + @PostMapping("/trigger/pause") + public void pauseTrigger(@RequestBody TriggerKeyDto triggerKey) throws SchedulerException { + schedulerService.pauseTrigger(new TriggerKey(triggerKey.name(), triggerKey.group())); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_SCHEDULER_PERMISSION)") + @PostMapping("/job/trigger") + public void triggerJob(@RequestBody JobKeyDto jobKeyDto, @RequestParam Long startAt) + throws SchedulerException { + schedulerService.triggerJob(new JobKey(jobKeyDto.name(), jobKeyDto.group()), new Date(startAt)); + } + + @PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_SCHEDULER_PERMISSION)") + @PutMapping("/job/update") + public void updateJob(@RequestBody TriggerKeyDto triggerKey, @RequestParam String cron) + throws SchedulerException { + schedulerService.updateCronTrigger(new TriggerKey(triggerKey.name(), triggerKey.group()), cron); + } +} diff --git a/backend/src/main/java/com/zl/mjga/controller/SignController.java b/backend/src/main/java/com/zl/mjga/controller/SignController.java new file mode 100644 index 0000000..8aed953 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/controller/SignController.java @@ -0,0 +1,43 @@ +package com.zl.mjga.controller; + +import com.zl.mjga.config.security.Jwt; +import com.zl.mjga.dto.sign.SignInDto; +import com.zl.mjga.dto.sign.SignUpDto; +import com.zl.mjga.service.SignService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class SignController { + + private final SignService signService; + + private final Jwt jwt; + + @ResponseStatus(HttpStatus.OK) + @PostMapping("/sign-in") + void signIn( + HttpServletRequest request, + HttpServletResponse response, + @RequestBody @Valid SignInDto signInDto) { + jwt.makeToken(request, response, String.valueOf(signService.signIn(signInDto))); + } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/sign-up") + void signUp(@RequestBody @Valid SignUpDto signUpDto) { + signService.signUp(signUpDto); + } + + @ResponseStatus(HttpStatus.OK) + @PostMapping("/sign-out") + void signOut(HttpServletRequest request, HttpServletResponse response) { + jwt.removeToken(request, response); + } +} diff --git a/backend/src/main/java/com/zl/mjga/dto/PageRequestDto.java b/backend/src/main/java/com/zl/mjga/dto/PageRequestDto.java new file mode 100644 index 0000000..6916ecd --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/PageRequestDto.java @@ -0,0 +1,129 @@ +package com.zl.mjga.dto; + +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.name; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.*; +import org.apache.commons.lang3.StringUtils; +import org.jooq.SortField; +import org.jooq.SortOrder; + +@Data +@NoArgsConstructor +public class PageRequestDto { + + public static final String REGEX = "^[a-zA-Z][a-zA-Z0-9_]*$"; + + public static final String SPACE = " "; + + private long page; + private long size; + + private Map sortBy = new HashMap<>(); + + public PageRequestDto(int page, int size) { + checkPageAndSize(page, size); + this.page = page; + this.size = size; + } + + public PageRequestDto(int page, int size, Map sortBy) { + checkPageAndSize(page, size); + this.page = page; + this.size = size; + this.sortBy = sortBy; + } + + @AllArgsConstructor + @Getter + public enum Direction { + ASC("ASC"), + DESC("DESC"); + + private final String keyword; + + public static Direction fromString(String value) { + try { + return Direction.valueOf(value.toUpperCase(Locale.US)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Invalid value '%s' for orders given; Has to be either 'desc' or 'asc' (case" + + " insensitive)", + value), + e); + } + } + } + + public static PageRequestDto of(int page, int size) { + return new PageRequestDto(page, size); + } + + public static PageRequestDto of(int page, int size, Map sortBy) { + return new PageRequestDto(page, size, sortBy); + } + + public List> getSortFields() { + return sortBy.entrySet().stream() + .map( + (entry) -> + field(name(entry.getKey())).sort(SortOrder.valueOf(entry.getValue().getKeyword()))) + .toList(); + } + + private void checkPageAndSize(int page, int size) { + if (page < 0) { + throw new IllegalArgumentException("Page index must not be less than zero"); + } + + if (size < 1) { + throw new IllegalArgumentException("Page size must not be less than one"); + } + } + + public long getOffset() { + if (page == 0) { + return 0; + } else { + return (page - 1) * size; + } + } + + public void setSortBy(String sortBy) { + this.sortBy = convertSortBy(sortBy); + } + + private Map convertSortBy(String sortBy) { + Map result = new HashMap<>(); + if (StringUtils.isEmpty(sortBy)) { + return result; + } + for (String fieldSpaceDirection : sortBy.split(",")) { + String[] fieldDirectionArray = fieldSpaceDirection.split(SPACE); + if (fieldDirectionArray.length != 2) { + throw new IllegalArgumentException( + String.format( + "Invalid sortBy field format %s. The expect format is [col1 asc,col2 desc]", + sortBy)); + } + String field = fieldDirectionArray[0]; + if (!verifySortField(field)) { + throw new IllegalArgumentException( + String.format("Invalid Sort field %s. Sort field must match %s", sortBy, REGEX)); + } + String direction = fieldDirectionArray[1]; + result.put(field, Direction.fromString(direction)); + } + return result; + } + + private static boolean verifySortField(String sortField) { + Pattern pattern = Pattern.compile(REGEX); + Matcher matcher = pattern.matcher(sortField); + return matcher.matches(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/dto/PageResponseDto.java b/backend/src/main/java/com/zl/mjga/dto/PageResponseDto.java new file mode 100644 index 0000000..246242a --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/PageResponseDto.java @@ -0,0 +1,22 @@ +package com.zl.mjga.dto; + +import jakarta.annotation.Nullable; +import lombok.*; + +@Data +public class PageResponseDto { + private long total; + private T data; + + public PageResponseDto(long total, @Nullable T data) { + if (total < 0) { + throw new IllegalArgumentException("total must not be less than zero"); + } + this.total = total; + this.data = data; + } + + public static PageResponseDto empty() { + return new PageResponseDto<>(0, null); + } +} diff --git a/backend/src/main/java/com/zl/mjga/dto/department/DepartmentBindDto.java b/backend/src/main/java/com/zl/mjga/dto/department/DepartmentBindDto.java new file mode 100644 index 0000000..92084df --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/department/DepartmentBindDto.java @@ -0,0 +1,6 @@ +package com.zl.mjga.dto.department; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record DepartmentBindDto(@NotNull Long userId, @NotNull List departmentIds) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/department/DepartmentQueryDto.java b/backend/src/main/java/com/zl/mjga/dto/department/DepartmentQueryDto.java new file mode 100644 index 0000000..17890fb --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/department/DepartmentQueryDto.java @@ -0,0 +1,16 @@ +package com.zl.mjga.dto.department; + +import com.zl.mjga.model.urp.BindState; +import lombok.*; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class DepartmentQueryDto { + private Long userId; + private String name; + private Boolean enable; + private BindState bindState = BindState.ALL; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/department/DepartmentRespDto.java b/backend/src/main/java/com/zl/mjga/dto/department/DepartmentRespDto.java new file mode 100644 index 0000000..221aad3 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/department/DepartmentRespDto.java @@ -0,0 +1,20 @@ +package com.zl.mjga.dto.department; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DepartmentRespDto { + @NotNull private Long id; + @NotEmpty private String name; + private Long parentId; + private String parentName; + private Boolean isBound; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/department/DepartmentUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/department/DepartmentUpsertDto.java new file mode 100644 index 0000000..1e53986 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/department/DepartmentUpsertDto.java @@ -0,0 +1,17 @@ +package com.zl.mjga.dto.department; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class DepartmentUpsertDto { + private Long id; + @NotEmpty private String name; + private Long parentId; + @NotNull private Boolean enable; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/permission/PermissionBindDto.java b/backend/src/main/java/com/zl/mjga/dto/permission/PermissionBindDto.java new file mode 100644 index 0000000..a5660a8 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/permission/PermissionBindDto.java @@ -0,0 +1,8 @@ +package com.zl.mjga.dto.permission; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record PermissionBindDto( + @NotNull Long roleId, @NotEmpty(message = "权限不能为空") List permissionIds) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/position/PositionBindDto.java b/backend/src/main/java/com/zl/mjga/dto/position/PositionBindDto.java new file mode 100644 index 0000000..0609d11 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/position/PositionBindDto.java @@ -0,0 +1,6 @@ +package com.zl.mjga.dto.position; + +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record PositionBindDto(@NotNull Long userId, @NotNull List positionIds) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/position/PositionQueryDto.java b/backend/src/main/java/com/zl/mjga/dto/position/PositionQueryDto.java new file mode 100644 index 0000000..bc470a2 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/position/PositionQueryDto.java @@ -0,0 +1,15 @@ +package com.zl.mjga.dto.position; + +import com.zl.mjga.model.urp.BindState; +import lombok.*; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class PositionQueryDto { + private Long userId; + private String name; + private BindState bindState = BindState.ALL; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/position/PositionRespDto.java b/backend/src/main/java/com/zl/mjga/dto/position/PositionRespDto.java new file mode 100644 index 0000000..d9f32cd --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/position/PositionRespDto.java @@ -0,0 +1,19 @@ +package com.zl.mjga.dto.position; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PositionRespDto { + @NotNull private Long id; + @NotEmpty private String name; + private Long parentId; + private Boolean isBound; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/position/PositionUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/position/PositionUpsertDto.java new file mode 100644 index 0000000..8124c84 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/position/PositionUpsertDto.java @@ -0,0 +1,17 @@ +package com.zl.mjga.dto.position; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class PositionUpsertDto { + private Long id; + @NotEmpty private String name; + private Long parentId; + @NotNull private Boolean enable; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/role/RoleBindDto.java b/backend/src/main/java/com/zl/mjga/dto/role/RoleBindDto.java new file mode 100644 index 0000000..777e824 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/role/RoleBindDto.java @@ -0,0 +1,8 @@ +package com.zl.mjga.dto.role; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record RoleBindDto( + @NotNull(message = "用户不能为空") Long userId, @NotEmpty(message = "角色不能为空") List roleIds) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/scheduler/JobKeyDto.java b/backend/src/main/java/com/zl/mjga/dto/scheduler/JobKeyDto.java new file mode 100644 index 0000000..17a22f4 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/scheduler/JobKeyDto.java @@ -0,0 +1,5 @@ +package com.zl.mjga.dto.scheduler; + +import jakarta.validation.constraints.NotEmpty; + +public record JobKeyDto(@NotEmpty String name, @NotEmpty String group) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/scheduler/JobTriggerDto.java b/backend/src/main/java/com/zl/mjga/dto/scheduler/JobTriggerDto.java new file mode 100644 index 0000000..92c2b6f --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/scheduler/JobTriggerDto.java @@ -0,0 +1,26 @@ +package com.zl.mjga.dto.scheduler; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class JobTriggerDto { + private String name; + private String group; + private String className; + private Map jobDataMap; + private String triggerName; + private String triggerGroup; + private String schedulerType; + private String cronExpression; + private long startTime; + private long endTime; + private long nextFireTime; + private long previousFireTime; + private String triggerState; + private Map triggerJobDataMap; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/scheduler/QueryDto.java b/backend/src/main/java/com/zl/mjga/dto/scheduler/QueryDto.java new file mode 100644 index 0000000..0cbaa31 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/scheduler/QueryDto.java @@ -0,0 +1,3 @@ +package com.zl.mjga.dto.scheduler; + +public record QueryDto(String name) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/scheduler/TriggerDto.java b/backend/src/main/java/com/zl/mjga/dto/scheduler/TriggerDto.java new file mode 100644 index 0000000..b40f794 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/scheduler/TriggerDto.java @@ -0,0 +1,21 @@ +package com.zl.mjga.dto.scheduler; + +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TriggerDto { + private String name; + private String group; + private String schedulerType; + private String cronExpression; + private long startTime; + private long endTime; + private long nextFireTime; + private long previousFireTime; + private Map jobDataMap; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/scheduler/TriggerKeyDto.java b/backend/src/main/java/com/zl/mjga/dto/scheduler/TriggerKeyDto.java new file mode 100644 index 0000000..3ba38e4 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/scheduler/TriggerKeyDto.java @@ -0,0 +1,5 @@ +package com.zl.mjga.dto.scheduler; + +import jakarta.validation.constraints.NotEmpty; + +public record TriggerKeyDto(@NotEmpty String name, @NotEmpty String group) {} diff --git a/backend/src/main/java/com/zl/mjga/dto/sign/SignInDto.java b/backend/src/main/java/com/zl/mjga/dto/sign/SignInDto.java new file mode 100644 index 0000000..4d092ff --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/sign/SignInDto.java @@ -0,0 +1,16 @@ +package com.zl.mjga.dto.sign; + +import jakarta.validation.constraints.NotEmpty; +import lombok.*; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class SignInDto { + + @NotEmpty private String username; + + @NotEmpty private String password; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/sign/SignUpDto.java b/backend/src/main/java/com/zl/mjga/dto/sign/SignUpDto.java new file mode 100644 index 0000000..26235f3 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/sign/SignUpDto.java @@ -0,0 +1,15 @@ +package com.zl.mjga.dto.sign; + +import jakarta.validation.constraints.NotEmpty; +import lombok.*; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class SignUpDto { + @NotEmpty private String username; + + @NotEmpty private String password; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/PermissionQueryDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/PermissionQueryDto.java new file mode 100644 index 0000000..8eb75d9 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/PermissionQueryDto.java @@ -0,0 +1,18 @@ +package com.zl.mjga.dto.urp; + +import com.zl.mjga.model.urp.BindState; +import java.util.List; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PermissionQueryDto { + + private Long roleId; + private Long permissionId; + private String permissionCode; + private String permissionName; + private List permissionIdList; + private BindState bindState = BindState.ALL; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/PermissionRespDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/PermissionRespDto.java new file mode 100644 index 0000000..b206b81 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/PermissionRespDto.java @@ -0,0 +1,14 @@ +package com.zl.mjga.dto.urp; + +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class PermissionRespDto { + private Long id; + private String code; + private String name; + private Boolean isBound; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/PermissionUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/PermissionUpsertDto.java new file mode 100644 index 0000000..963101f --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/PermissionUpsertDto.java @@ -0,0 +1,15 @@ +package com.zl.mjga.dto.urp; + +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class PermissionUpsertDto { + private Long id; + @NotEmpty private String code; + @NotEmpty private String name; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/RoleDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/RoleDto.java new file mode 100644 index 0000000..93e7dd7 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/RoleDto.java @@ -0,0 +1,17 @@ +package com.zl.mjga.dto.urp; + +import java.util.LinkedList; +import java.util.List; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class RoleDto { + private Long id; + private String code; + private String name; + private Boolean isBound; + @Builder.Default List permissions = new LinkedList<>(); +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/RoleQueryDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/RoleQueryDto.java new file mode 100644 index 0000000..042d749 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/RoleQueryDto.java @@ -0,0 +1,18 @@ +package com.zl.mjga.dto.urp; + +import com.zl.mjga.model.urp.BindState; +import java.util.List; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class RoleQueryDto { + + private Long userId; + private Long roleId; + private String roleCode; + private String roleName; + private List roleIdList; + private BindState bindState = BindState.ALL; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/RoleUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/RoleUpsertDto.java new file mode 100644 index 0000000..fe2b1fa --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/RoleUpsertDto.java @@ -0,0 +1,15 @@ +package com.zl.mjga.dto.urp; + +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class RoleUpsertDto { + private Long id; + @NotEmpty private String code; + @NotEmpty private String name; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/UserQueryDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/UserQueryDto.java new file mode 100644 index 0000000..76ebe21 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/UserQueryDto.java @@ -0,0 +1,10 @@ +package com.zl.mjga.dto.urp; + +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserQueryDto { + private String username; +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java new file mode 100644 index 0000000..11f8aa6 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/UserRolePermissionDto.java @@ -0,0 +1,34 @@ +package com.zl.mjga.dto.urp; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Builder +public class UserRolePermissionDto { + private Long id; + private String username; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String password; + + private Boolean enable; + @Builder.Default private List roles = new LinkedList<>(); + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private OffsetDateTime createTime; + + public Set getPermissions() { + return roles.stream() + .flatMap((roleDto) -> roleDto.getPermissions().stream()) + .collect(Collectors.toSet()); + } +} diff --git a/backend/src/main/java/com/zl/mjga/dto/urp/UserUpsertDto.java b/backend/src/main/java/com/zl/mjga/dto/urp/UserUpsertDto.java new file mode 100644 index 0000000..51e6f0f --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/dto/urp/UserUpsertDto.java @@ -0,0 +1,17 @@ +package com.zl.mjga.dto.urp; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class UserUpsertDto { + private Long id; + @NotEmpty private String username; + private String password; + @NotNull private Boolean enable; +} diff --git a/backend/src/main/java/com/zl/mjga/exception/BusinessException.java b/backend/src/main/java/com/zl/mjga/exception/BusinessException.java new file mode 100644 index 0000000..0c460ad --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/exception/BusinessException.java @@ -0,0 +1,25 @@ +package com.zl.mjga.exception; + +public class BusinessException extends RuntimeException { + + @java.io.Serial private static final long serialVersionUID = -2119302295305964305L; + + public BusinessException() {} + + public BusinessException(String message) { + super(message); + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + } + + public BusinessException(Throwable cause) { + super(cause); + } + + public BusinessException( + String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/backend/src/main/java/com/zl/mjga/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/zl/mjga/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..b3cd7f5 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/exception/GlobalExceptionHandler.java @@ -0,0 +1,90 @@ +package com.zl.mjga.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.lang.Nullable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.firewall.RequestRejectedException; +import org.springframework.web.ErrorResponseException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + @ExceptionHandler(value = {BusinessException.class}) + public ResponseEntity handleBusinessException(BusinessException ex, WebRequest request) { + log.error("Business Error Handled ===> ", ex); + ErrorResponseException errorResponseException = + new ErrorResponseException( + HttpStatus.INTERNAL_SERVER_ERROR, + ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()), + ex.getCause()); + return handleExceptionInternal( + errorResponseException, + errorResponseException.getBody(), + errorResponseException.getHeaders(), + errorResponseException.getStatusCode(), + request); + } + + @SuppressWarnings("NullableProblems") + @Override + @Nullable public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + log.error("MethodArgumentNotValidException Handled ===> ", ex); + ErrorResponseException errorResponseException = + new ErrorResponseException( + status, ProblemDetail.forStatusAndDetail(status, ex.getMessage()), ex.getCause()); + return handleExceptionInternal( + errorResponseException, + errorResponseException.getBody(), + errorResponseException.getHeaders(), + errorResponseException.getStatusCode(), + request); + } + + @ExceptionHandler(value = {RequestRejectedException.class}) + public ResponseEntity handleRequestRejectedException( + RequestRejectedException ex, WebRequest request) { + log.error("RequestRejectedException Handled ===> ", ex); + ErrorResponseException errorResponseException = + new ErrorResponseException( + HttpStatus.BAD_REQUEST, + ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage()), + ex.getCause()); + return handleExceptionInternal( + errorResponseException, + errorResponseException.getBody(), + errorResponseException.getHeaders(), + errorResponseException.getStatusCode(), + request); + } + + @ExceptionHandler(value = {AccessDeniedException.class}) + public ResponseEntity handleAccessDenied(AccessDeniedException ex) { + throw ex; + } + + @ExceptionHandler(value = {Throwable.class}) + public ResponseEntity handleException(Throwable ex, WebRequest request) { + log.error("System Error Handled ===> ", ex); + ErrorResponseException errorResponseException = + new ErrorResponseException( + HttpStatus.INTERNAL_SERVER_ERROR, + ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "System Error"), + ex.getCause()); + return handleExceptionInternal( + errorResponseException, + errorResponseException.getBody(), + errorResponseException.getHeaders(), + errorResponseException.getStatusCode(), + request); + } +} diff --git a/backend/src/main/java/com/zl/mjga/job/DataBackupJob.java b/backend/src/main/java/com/zl/mjga/job/DataBackupJob.java new file mode 100644 index 0000000..0a6ae8c --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/job/DataBackupJob.java @@ -0,0 +1,19 @@ +package com.zl.mjga.job; + +import java.text.MessageFormat; +import lombok.extern.slf4j.Slf4j; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@Slf4j +public class DataBackupJob implements Job { + + @Override + public void execute(JobExecutionContext context) { + String userId = context.getJobDetail().getJobDataMap().getString("roleId"); + log.info( + MessageFormat.format( + "Job execute: JobName {0} Param {1} Thread: {2}", + getClass(), userId, Thread.currentThread().getName())); + } +} diff --git a/backend/src/main/java/com/zl/mjga/job/EmailJob.java b/backend/src/main/java/com/zl/mjga/job/EmailJob.java new file mode 100644 index 0000000..87786ba --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/job/EmailJob.java @@ -0,0 +1,19 @@ +package com.zl.mjga.job; + +import java.text.MessageFormat; +import lombok.extern.slf4j.Slf4j; +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +@Slf4j +public class EmailJob implements Job { + + @Override + public void execute(JobExecutionContext context) { + String userEmail = context.getJobDetail().getJobDataMap().getString("userEmail"); + log.info( + MessageFormat.format( + "Job execute: JobName {0} Param {1} Thread: {2}", + getClass(), userEmail, Thread.currentThread().getName())); + } +} diff --git a/backend/src/main/java/com/zl/mjga/model/urp/BindState.java b/backend/src/main/java/com/zl/mjga/model/urp/BindState.java new file mode 100644 index 0000000..3bbee0c --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/model/urp/BindState.java @@ -0,0 +1,7 @@ +package com.zl.mjga.model.urp; + +public enum BindState { + BIND, + UNBIND, + ALL; +} diff --git a/backend/src/main/java/com/zl/mjga/model/urp/EPermission.java b/backend/src/main/java/com/zl/mjga/model/urp/EPermission.java new file mode 100644 index 0000000..d6fb35f --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/model/urp/EPermission.java @@ -0,0 +1,12 @@ +package com.zl.mjga.model.urp; + +public enum EPermission { + READ_POSITION_PERMISSION, + WRITE_POSITION_PERMISSION, + READ_DEPARTMENT_PERMISSION, + WRITE_DEPARTMENT_PERMISSION, + READ_SCHEDULER_PERMISSION, + WRITE_SCHEDULER_PERMISSION, + WRITE_USER_ROLE_PERMISSION, + READ_USER_ROLE_PERMISSION +} diff --git a/backend/src/main/java/com/zl/mjga/model/urp/ERole.java b/backend/src/main/java/com/zl/mjga/model/urp/ERole.java new file mode 100644 index 0000000..fa8a1bd --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/model/urp/ERole.java @@ -0,0 +1,6 @@ +package com.zl.mjga.model.urp; + +public enum ERole { + ADMIN, + GENERAL +} diff --git a/backend/src/main/java/com/zl/mjga/model/urp/SchedulerType.java b/backend/src/main/java/com/zl/mjga/model/urp/SchedulerType.java new file mode 100644 index 0000000..fc8fae9 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/model/urp/SchedulerType.java @@ -0,0 +1,8 @@ +package com.zl.mjga.model.urp; + +public enum SchedulerType { + CRON, + SIMPLE, + CALENDAR, + DAILY +} diff --git a/backend/src/main/java/com/zl/mjga/repository/DepartmentRepository.java b/backend/src/main/java/com/zl/mjga/repository/DepartmentRepository.java new file mode 100644 index 0000000..850ddad --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/DepartmentRepository.java @@ -0,0 +1,67 @@ +package com.zl.mjga.repository; + +import static org.jooq.generated.mjga.Tables.*; +import static org.jooq.impl.DSL.noCondition; +import static org.jooq.impl.DSL.noField; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.department.DepartmentQueryDto; +import org.apache.commons.lang3.StringUtils; +import org.jooq.*; +import org.jooq.Record; +import org.jooq.generated.mjga.tables.Department; +import org.jooq.generated.mjga.tables.daos.DepartmentDao; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class DepartmentRepository extends DepartmentDao { + + @Autowired + public DepartmentRepository(Configuration configuration) { + super(configuration); + } + + public Result pageFetchBy( + PageRequestDto pageRequestDto, DepartmentQueryDto departmentQueryDto) { + Department parent = DEPARTMENT.as("parent"); + return ctx() + .select( + DEPARTMENT.asterisk(), + parent.NAME.as("parent_name"), + departmentQueryDto.getUserId() != null + ? DSL.when( + DEPARTMENT.ID.in(selectUsersDepartment(departmentQueryDto.getUserId())), + true) + .otherwise(false) + .as("is_bound") + : noField(), + DSL.count().over().as("total_department").convertFrom(Long::valueOf)) + .from(DEPARTMENT) + .leftJoin(parent) + .on(parent.ID.eq(DEPARTMENT.PARENT_ID)) + .where( + switch (departmentQueryDto.getBindState()) { + case BIND -> DEPARTMENT.ID.in(selectUsersDepartment(departmentQueryDto.getUserId())); + case UNBIND -> + DEPARTMENT.ID.notIn(selectUsersDepartment(departmentQueryDto.getUserId())); + case ALL -> noCondition(); + }) + .and( + StringUtils.isNotEmpty(departmentQueryDto.getName()) + ? DEPARTMENT.NAME.like("%" + departmentQueryDto.getName() + "%") + : noCondition()) + .orderBy(pageRequestDto.getSortFields()) + .limit(pageRequestDto.getSize()) + .offset(pageRequestDto.getOffset()) + .fetch(); + } + + private SelectConditionStep> selectUsersDepartment(Long userId) { + return DSL.select(USER.department().ID) + .from(USER) + .innerJoin(USER.department()) + .where(USER.ID.eq(userId)); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/PermissionRepository.java b/backend/src/main/java/com/zl/mjga/repository/PermissionRepository.java new file mode 100644 index 0000000..c956e99 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/PermissionRepository.java @@ -0,0 +1,81 @@ +package com.zl.mjga.repository; + +import static org.jooq.generated.mjga.tables.Permission.PERMISSION; +import static org.jooq.generated.mjga.tables.Role.ROLE; +import static org.jooq.impl.DSL.*; +import static org.jooq.impl.DSL.noField; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.urp.PermissionQueryDto; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.jooq.*; +import org.jooq.Record; +import org.jooq.generated.mjga.tables.daos.PermissionDao; +import org.jooq.generated.mjga.tables.pojos.Permission; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class PermissionRepository extends PermissionDao { + + @Autowired + public PermissionRepository(Configuration configuration) { + super(configuration); + } + + public Result pageFetchBy( + PageRequestDto pageRequestDto, PermissionQueryDto permissionQueryDto) { + return ctx() + .select( + asterisk(), + permissionQueryDto.getRoleId() != null + ? when( + PERMISSION.ID.in(selectRolesPermissionIds(permissionQueryDto.getRoleId())), + true) + .otherwise(false) + .as("is_bound") + : noField(), + DSL.count().over().as("total_permission")) + .from(PERMISSION) + .where( + switch (permissionQueryDto.getBindState()) { + case BIND -> + PERMISSION.ID.in(selectRolesPermissionIds(permissionQueryDto.getRoleId())); + case UNBIND -> + PERMISSION.ID.notIn(selectRolesPermissionIds(permissionQueryDto.getRoleId())); + case ALL -> noCondition(); + }) + .and( + permissionQueryDto.getPermissionId() == null + ? noCondition() + : PERMISSION.ID.eq(permissionQueryDto.getPermissionId())) + .and( + StringUtils.isEmpty(permissionQueryDto.getPermissionName()) + ? noCondition() + : PERMISSION.NAME.like("%" + permissionQueryDto.getPermissionName() + "%")) + .and( + StringUtils.isEmpty(permissionQueryDto.getPermissionName()) + ? noCondition() + : PERMISSION.CODE.eq(permissionQueryDto.getPermissionCode())) + .orderBy(pageRequestDto.getSortFields()) + .limit(pageRequestDto.getSize()) + .offset(pageRequestDto.getOffset()) + .fetch(); + } + + private SelectConditionStep> selectRolesPermissionIds(Long roleId) { + return DSL.select(ROLE.permission().ID) + .from(ROLE) + .leftJoin(ROLE.permission()) + .where(ROLE.ID.eq(roleId)); + } + + public List selectByPermissionIdIn(List permissionIdList) { + return ctx() + .selectFrom(PERMISSION) + .where(PERMISSION.ID.in(permissionIdList)) + .fetchInto(Permission.class); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/PositionRepository.java b/backend/src/main/java/com/zl/mjga/repository/PositionRepository.java new file mode 100644 index 0000000..0a951d8 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/PositionRepository.java @@ -0,0 +1,60 @@ +package com.zl.mjga.repository; + +import static org.jooq.generated.mjga.Tables.*; +import static org.jooq.impl.DSL.noCondition; +import static org.jooq.impl.DSL.noField; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.position.PositionQueryDto; +import org.apache.commons.lang3.StringUtils; +import org.jooq.*; +import org.jooq.Record; +import org.jooq.generated.mjga.tables.daos.PositionDao; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class PositionRepository extends PositionDao { + + @Autowired + public PositionRepository(Configuration configuration) { + super(configuration); + } + + public Result pageFetchBy( + PageRequestDto pageRequestDto, PositionQueryDto positionQueryDto) { + return ctx() + .select( + POSITION.asterisk(), + positionQueryDto.getUserId() != null + ? DSL.when(POSITION.ID.in(selectUsersPosition(positionQueryDto.getUserId())), true) + .otherwise(false) + .as("is_bound") + : noField(), + DSL.count().over().as("total_position").convertFrom(Long::valueOf)) + .from(POSITION) + .where( + switch (positionQueryDto.getBindState()) { + case BIND -> POSITION.ID.in(selectUsersPosition(positionQueryDto.getUserId())); + case UNBIND -> POSITION.ID.notIn(selectUsersPosition(positionQueryDto.getUserId())); + case ALL -> noCondition(); + }) + .and( + StringUtils.isNotEmpty(positionQueryDto.getName()) + ? POSITION.NAME.like("%" + positionQueryDto.getName() + "%") + : noCondition()) + .orderBy(pageRequestDto.getSortFields()) + .limit(pageRequestDto.getSize()) + .offset(pageRequestDto.getOffset()) + .fetch(); + } + + private SelectConditionStep> selectUsersPosition(Long userId) { + return ctx() + .select(USER.position().ID) + .from(USER) + .innerJoin(USER.position()) + .where(USER.ID.eq(userId)); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/QrtzJobRepository.java b/backend/src/main/java/com/zl/mjga/repository/QrtzJobRepository.java new file mode 100644 index 0000000..9ad9ef6 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/QrtzJobRepository.java @@ -0,0 +1,46 @@ +package com.zl.mjga.repository; + +import static org.jooq.generated.public_.Tables.*; +import static org.jooq.impl.DSL.noCondition; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.scheduler.QueryDto; +import org.apache.commons.lang3.StringUtils; +import org.jooq.*; +import org.jooq.Record; +import org.jooq.generated.public_.tables.daos.QrtzJobDetailsDao; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class QrtzJobRepository extends QrtzJobDetailsDao { + + @Autowired + public QrtzJobRepository(Configuration configuration) { + super(configuration); + } + + public Result fetchPageWithJobAndTriggerBy( + PageRequestDto pageRequestDto, QueryDto queryDto) { + return ctx() + .select( + QRTZ_JOB_DETAILS.asterisk(), + QRTZ_JOB_DETAILS.qrtzTriggers().asterisk(), + QRTZ_JOB_DETAILS.qrtzTriggers().qrtzCronTriggers().asterisk(), + QRTZ_JOB_DETAILS.qrtzTriggers().qrtzSimpleTriggers().asterisk(), + DSL.count().over().as("total_job")) + .from(QRTZ_JOB_DETAILS) + .leftJoin(QRTZ_JOB_DETAILS.qrtzTriggers()) + .leftJoin(QRTZ_JOB_DETAILS.qrtzTriggers().qrtzCronTriggers()) + .leftJoin(QRTZ_JOB_DETAILS.qrtzTriggers().qrtzSimpleTriggers()) + .where( + StringUtils.isNotEmpty(queryDto.name()) + ? QRTZ_JOB_DETAILS.SCHED_NAME.eq(queryDto.name()) + : noCondition()) + .orderBy(pageRequestDto.getSortFields()) + .limit(pageRequestDto.getSize()) + .offset(pageRequestDto.getOffset()) + .fetch(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/RolePermissionMapRepository.java b/backend/src/main/java/com/zl/mjga/repository/RolePermissionMapRepository.java new file mode 100644 index 0000000..f538bea --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/RolePermissionMapRepository.java @@ -0,0 +1,33 @@ +package com.zl.mjga.repository; + +import static org.jooq.generated.mjga.tables.RolePermissionMap.ROLE_PERMISSION_MAP; + +import java.util.List; +import org.jooq.Configuration; +import org.jooq.generated.mjga.tables.daos.RolePermissionMapDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public class RolePermissionMapRepository extends RolePermissionMapDao { + + @Autowired + public RolePermissionMapRepository(Configuration configuration) { + super(configuration); + } + + @Transactional + public void deleteByRoleId(Long roleId) { + ctx().deleteFrom(ROLE_PERMISSION_MAP).where(ROLE_PERMISSION_MAP.ROLE_ID.eq(roleId)).execute(); + } + + @Transactional + public void deleteBy(Long roleId, List permissionIdList) { + ctx() + .deleteFrom(ROLE_PERMISSION_MAP) + .where(ROLE_PERMISSION_MAP.ROLE_ID.eq(roleId)) + .and(ROLE_PERMISSION_MAP.PERMISSION_ID.in(permissionIdList)) + .execute(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/RoleRepository.java b/backend/src/main/java/com/zl/mjga/repository/RoleRepository.java new file mode 100644 index 0000000..eb46a55 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/RoleRepository.java @@ -0,0 +1,84 @@ +package com.zl.mjga.repository; + +import static org.jooq.generated.mjga.Tables.USER; +import static org.jooq.generated.mjga.tables.Role.ROLE; +import static org.jooq.impl.DSL.*; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.urp.RoleQueryDto; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.jooq.*; +import org.jooq.Record; +import org.jooq.generated.mjga.tables.daos.RoleDao; +import org.jooq.generated.mjga.tables.pojos.Permission; +import org.jooq.generated.mjga.tables.pojos.Role; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class RoleRepository extends RoleDao { + + @Autowired + public RoleRepository(Configuration configuration) { + super(configuration); + } + + public List selectByRoleCodeIn(List roleCodeList) { + return ctx().selectFrom(ROLE).where(ROLE.CODE.in(roleCodeList)).fetchInto(Role.class); + } + + public List selectByRoleIdIn(List roleIdList) { + return ctx().selectFrom(ROLE).where(ROLE.ID.in(roleIdList)).fetchInto(Role.class); + } + + public Result pageFetchBy(PageRequestDto pageRequestDto, RoleQueryDto roleQueryDto) { + return ctx() + .select( + asterisk(), + roleQueryDto.getUserId() != null + ? when(ROLE.ID.in(selectUsersRoleIds(roleQueryDto.getUserId())), true) + .otherwise(false) + .as("is_bound") + : noField(), + multiset(select(ROLE.permission().asterisk()).from(ROLE.permission())) + .convertFrom(r -> r.into(Permission.class)) + .as("permissions"), + DSL.count(ROLE.ID).over().as("total_role")) + .from(ROLE) + .where( + switch (roleQueryDto.getBindState()) { + case BIND -> ROLE.ID.in(selectUsersRoleIds(roleQueryDto.getUserId())); + case UNBIND -> ROLE.ID.notIn(selectUsersRoleIds(roleQueryDto.getUserId())); + case ALL -> noCondition(); + }) + .and( + roleQueryDto.getRoleId() == null ? noCondition() : ROLE.ID.eq(roleQueryDto.getRoleId())) + .and( + StringUtils.isEmpty(roleQueryDto.getRoleName()) + ? noCondition() + : ROLE.NAME.like("%" + roleQueryDto.getRoleName() + "%")) + .and( + StringUtils.isEmpty(roleQueryDto.getRoleCode()) + ? noCondition() + : ROLE.CODE.eq(roleQueryDto.getRoleCode())) + .orderBy(pageRequestDto.getSortFields()) + .limit(pageRequestDto.getSize()) + .offset(pageRequestDto.getOffset()) + .fetch(); + } + + private SelectConditionStep> selectUsersRoleIds(Long userId) { + return DSL.select(USER.role().ID).from(USER).innerJoin(USER.role()).where(USER.ID.eq(userId)); + } + + public Result fetchUniqueRoleWithPermission(Long roleId) { + return ctx() + .select(ROLE.asterisk(), ROLE.permission().asterisk()) + .from(ROLE, ROLE.permission()) + .where(ROLE.ID.eq(roleId)) + .orderBy(ROLE.ID) + .fetch(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/UserDepartmentMapRepository.java b/backend/src/main/java/com/zl/mjga/repository/UserDepartmentMapRepository.java new file mode 100644 index 0000000..65379e5 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/UserDepartmentMapRepository.java @@ -0,0 +1,14 @@ +package com.zl.mjga.repository; + +import org.jooq.Configuration; +import org.jooq.generated.mjga.tables.daos.UserDepartmentMapDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class UserDepartmentMapRepository extends UserDepartmentMapDao { + @Autowired + public UserDepartmentMapRepository(Configuration configuration) { + super(configuration); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/UserPositionMapRepository.java b/backend/src/main/java/com/zl/mjga/repository/UserPositionMapRepository.java new file mode 100644 index 0000000..77625c4 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/UserPositionMapRepository.java @@ -0,0 +1,14 @@ +package com.zl.mjga.repository; + +import org.jooq.Configuration; +import org.jooq.generated.mjga.tables.daos.UserPositionMapDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class UserPositionMapRepository extends UserPositionMapDao { + @Autowired + public UserPositionMapRepository(Configuration configuration) { + super(configuration); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/UserRepository.java b/backend/src/main/java/com/zl/mjga/repository/UserRepository.java new file mode 100644 index 0000000..aba2782 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/UserRepository.java @@ -0,0 +1,151 @@ +package com.zl.mjga.repository; + +import static org.jooq.generated.mjga.Tables.*; +import static org.jooq.generated.mjga.tables.User.USER; +import static org.jooq.impl.DSL.*; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.urp.PermissionRespDto; +import com.zl.mjga.dto.urp.RoleDto; +import com.zl.mjga.dto.urp.UserQueryDto; +import com.zl.mjga.dto.urp.UserRolePermissionDto; +import org.apache.commons.lang3.StringUtils; +import org.jooq.*; +import org.jooq.Record; +import org.jooq.generated.mjga.tables.daos.*; +import org.jooq.generated.mjga.tables.pojos.User; +import org.jooq.impl.DSL; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public class UserRepository extends UserDao { + + @Autowired + public UserRepository(Configuration configuration) { + super(configuration); + } + + @Transactional + public void mergeWithoutNullFieldBy(User user) { + ctx() + .mergeInto(USER) + .using( + select( + value(user.getId()).as("id"), + value(user.getUsername()).as("username"), + value(user.getPassword()).as("password"), + value(user.getEnable()).as("enable")) + .asTable("newUser")) + .on(USER.ID.eq(DSL.field(DSL.name("newUser", "id"), Long.class))) + .whenMatchedThenUpdate() + .set(USER.USERNAME, DSL.field(DSL.name("newUser", "username"), String.class)) + .set( + USER.PASSWORD, + StringUtils.isNotEmpty(user.getPassword()) + ? DSL.field(DSL.name("newUser", "password"), String.class) + : USER.PASSWORD) + .set(USER.ENABLE, DSL.field(DSL.name("newUser", "enable"), Boolean.class)) + .whenNotMatchedThenInsert(USER.USERNAME, USER.PASSWORD, USER.ENABLE) + .values( + DSL.field(DSL.name("newUser", "username"), String.class), + DSL.field(DSL.name("newUser", "password"), String.class), + DSL.field(DSL.name("newUser", "enable"), Boolean.class)) + .execute(); + } + + public Result pageFetchBy(PageRequestDto pageRequestDto, UserQueryDto userQueryDto) { + return ctx() + .select(asterisk(), DSL.count().over().as("total_user")) + .from(USER) + .where( + userQueryDto.getUsername() != null + ? USER.USERNAME.like("%" + userQueryDto.getUsername() + "%") + : noCondition()) + .orderBy(pageRequestDto.getSortFields()) + .limit(pageRequestDto.getSize()) + .offset(pageRequestDto.getOffset()) + .fetch(); + } + + public UserRolePermissionDto fetchUniqueUserDtoWithNestedRolePermissionBy(Long userId) { + return ctx() + .select( + USER.asterisk(), + multiset( + select( + USER.role().asterisk(), + multiset( + select(USER.role().permission().asterisk()) + .from(USER.role().permission())) + .convertFrom( + r -> r.map((record) -> record.into(PermissionRespDto.class))) + .as("permissions")) + .from(USER.role())) + .convertFrom(r -> r.map((record) -> record.into(RoleDto.class))) + .as("roles")) + .from(USER) + .where(USER.ID.eq(userId)) + .fetchOneInto(UserRolePermissionDto.class); + } + + // public UserRolePermissionDto fetchUniqueUserDtoWithNestedRolePermissionBy(Long roleId) { + // return ctx() + // .select( + // USER.asterisk(), + // multiset( + // select( + // ROLE.asterisk(), + // multiset( + // select(PERMISSION.asterisk()) + // .from(ROLE_PERMISSION_MAP) + // .leftJoin(PERMISSION) + // .on(ROLE_PERMISSION_MAP.PERMISSION_ID.eq(PERMISSION.ID)) + // .where(ROLE_PERMISSION_MAP.ROLE_ID.eq(ROLE.ID))) + // .convertFrom( + // r -> r.map((record) -> + // record.into(PermissionRespDto.class))) + // .as("permissions")) + // .from(USER_ROLE_MAP) + // .leftJoin(ROLE) + // .on(USER_ROLE_MAP.ROLE_ID.eq(ROLE.ID)) + // .where(USER.ID.eq(USER_ROLE_MAP.USER_ID))) + // .convertFrom(r -> r.map((record) -> record.into(RoleDto.class))) + // .as("roles")) + // .from(USER) + // .where(USER.ID.eq(roleId)) + // .fetchOneInto(UserRolePermissionDto.class); + // } + + public Result fetchUniqueUserWithRolePermissionBy(Long userId) { + return ctx() + .select(USER.asterisk(), USER.role().asterisk(), USER.role().permission().asterisk()) + .from(USER) + .leftJoin(USER.role()) + .leftJoin(USER.role().permission()) + .where(USER.ID.eq(userId)) + .fetch(); + } + + // public Result fetchUniqueUserWithRolePermissionBy(Long roleId) { + // return ctx() + // .select() + // .from(USER) + // .leftJoin(USER_ROLE_MAP) + // .on(USER.ID.eq(USER_ROLE_MAP.USER_ID)) + // .leftJoin(ROLE) + // .on(USER_ROLE_MAP.ROLE_ID.eq(ROLE.ID)) + // .leftJoin(ROLE_PERMISSION_MAP) + // .on(ROLE.ID.eq(ROLE_PERMISSION_MAP.ROLE_ID)) + // .leftJoin(PERMISSION) + // .on(ROLE_PERMISSION_MAP.PERMISSION_ID.eq(PERMISSION.ID)) + // .where(USER.ID.eq(roleId)) + // .fetch(); + // } + + @Transactional + public void deleteByUsername(String username) { + ctx().delete(USER).where(USER.USERNAME.eq(username)).execute(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/repository/UserRoleMapRepository.java b/backend/src/main/java/com/zl/mjga/repository/UserRoleMapRepository.java new file mode 100644 index 0000000..b3f44c8 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/repository/UserRoleMapRepository.java @@ -0,0 +1,33 @@ +package com.zl.mjga.repository; + +import static org.jooq.generated.mjga.tables.UserRoleMap.USER_ROLE_MAP; + +import java.util.List; +import org.jooq.Configuration; +import org.jooq.generated.mjga.tables.daos.UserRoleMapDao; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public class UserRoleMapRepository extends UserRoleMapDao { + + @Autowired + public UserRoleMapRepository(Configuration configuration) { + super(configuration); + } + + @Transactional + public void deleteByUserId(Long userId) { + ctx().deleteFrom(USER_ROLE_MAP).where(USER_ROLE_MAP.USER_ID.eq(userId)).execute(); + } + + @Transactional + public void deleteBy(Long userId, List roleIdList) { + ctx() + .deleteFrom(USER_ROLE_MAP) + .where(USER_ROLE_MAP.USER_ID.eq(userId)) + .and(USER_ROLE_MAP.ROLE_ID.in(roleIdList)) + .execute(); + } +} diff --git a/backend/src/main/java/com/zl/mjga/service/CacheService.java b/backend/src/main/java/com/zl/mjga/service/CacheService.java new file mode 100644 index 0000000..bf5ba9a --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/CacheService.java @@ -0,0 +1,28 @@ +package com.zl.mjga.service; + +import com.zl.mjga.config.cache.CacheConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class CacheService { + @Cacheable(value = CacheConfig.VERIFY_CODE, key = "{#identify}", unless = "#result == null") + public String getVerifyCodeBy(String identify) { + return null; + } + + @CachePut(value = CacheConfig.VERIFY_CODE, key = "{#identify}") + public String upsertVerifyCodeBy(String identify, String value) { + return value; + } + + @CacheEvict(value = CacheConfig.VERIFY_CODE, key = "{#identify}") + public void removeVerifyCodeBy(String identify) {} + + @CacheEvict(value = CacheConfig.VERIFY_CODE, allEntries = true) + public void clearAllVerifyCode() {} +} diff --git a/backend/src/main/java/com/zl/mjga/service/DepartmentService.java b/backend/src/main/java/com/zl/mjga/service/DepartmentService.java new file mode 100644 index 0000000..143ae87 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/DepartmentService.java @@ -0,0 +1,47 @@ +package com.zl.mjga.service; + +import static org.jooq.generated.mjga.tables.Department.DEPARTMENT; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.department.DepartmentQueryDto; +import com.zl.mjga.dto.department.DepartmentRespDto; +import com.zl.mjga.repository.DepartmentRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jooq.Record; +import org.jooq.Result; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class DepartmentService { + + private final DepartmentRepository departmentRepository; + + public PageResponseDto> pageQueryDepartment( + PageRequestDto pageRequestDto, DepartmentQueryDto departmentQueryDto) { + Result records = departmentRepository.pageFetchBy(pageRequestDto, departmentQueryDto); + if (records.isEmpty()) { + return PageResponseDto.empty(); + } + List departments = + records.map( + record -> { + return DepartmentRespDto.builder() + .id(record.getValue(DEPARTMENT.ID)) + .name(record.getValue(DEPARTMENT.NAME)) + .parentId(record.getValue(DEPARTMENT.PARENT_ID)) + .isBound( + record.field("is_bound") != null + ? record.get("is_bound", Boolean.class) + : null) + .parentName(record.get("parent_name", String.class)) + .build(); + }); + Long totalDepartment = records.get(0).getValue("total_department", Long.class); + return new PageResponseDto<>(totalDepartment, departments); + } +} diff --git a/backend/src/main/java/com/zl/mjga/service/IdentityAccessService.java b/backend/src/main/java/com/zl/mjga/service/IdentityAccessService.java new file mode 100644 index 0000000..4330c9e --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/IdentityAccessService.java @@ -0,0 +1,291 @@ +package com.zl.mjga.service; + +import static org.jooq.generated.mjga.tables.Permission.PERMISSION; +import static org.jooq.generated.mjga.tables.Role.ROLE; +import static org.jooq.generated.mjga.tables.User.USER; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.department.DepartmentBindDto; +import com.zl.mjga.dto.position.PositionBindDto; +import com.zl.mjga.dto.urp.*; +import com.zl.mjga.exception.BusinessException; +import com.zl.mjga.model.urp.ERole; +import com.zl.mjga.repository.*; +import java.util.*; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.jooq.Record; +import org.jooq.Result; +import org.jooq.generated.mjga.tables.pojos.*; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.BeanUtils; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class IdentityAccessService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final UserRoleMapRepository userRoleMapRepository; + private final PermissionRepository permissionRepository; + private final RolePermissionMapRepository rolePermissionMapRepository; + private final UserDepartmentMapRepository userDepartmentMapRepository; + private final UserPositionMapRepository userPositionMapRepository; + private final PasswordEncoder passwordEncoder; + + public void upsertUser(UserUpsertDto userUpsertDto) { + User user = new User(); + BeanUtils.copyProperties(userUpsertDto, user); + if (StringUtils.isNotEmpty(userUpsertDto.getPassword())) { + user.setPassword(passwordEncoder.encode(userUpsertDto.getPassword())); + } + userRepository.mergeWithoutNullFieldBy(user); + } + + public void upsertRole(RoleUpsertDto roleUpsertDto) { + Role role = new Role(); + BeanUtils.copyProperties(roleUpsertDto, role); + roleRepository.merge(role); + } + + public void upsertPermission(PermissionUpsertDto permissionUpsertDto) { + Permission permission = new Permission(); + BeanUtils.copyProperties(permissionUpsertDto, permission); + permissionRepository.merge(permission); + } + + public PageResponseDto> pageQueryUser( + PageRequestDto pageRequestDto, UserQueryDto userQueryDto) { + Result userRecords = userRepository.pageFetchBy(pageRequestDto, userQueryDto); + if (userRecords.isEmpty()) { + return PageResponseDto.empty(); + } + List userRolePermissionDtoList = + userRecords.stream() + .map((record) -> queryUniqueUserWithRolePermission(record.getValue(USER.ID))) + .toList(); + return new PageResponseDto<>( + userRecords.get(0).getValue("total_user", Integer.class), userRolePermissionDtoList); + } + + public @Nullable UserRolePermissionDto queryUniqueUserWithRolePermission(Long userId) { + return userRepository.fetchUniqueUserDtoWithNestedRolePermissionBy(userId); + } + + public PageResponseDto> pageQueryRole( + PageRequestDto pageRequestDto, RoleQueryDto roleQueryDto) { + Result roleRecords = roleRepository.pageFetchBy(pageRequestDto, roleQueryDto); + if (roleRecords.isEmpty()) { + return PageResponseDto.empty(); + } + List roleDtoList = + roleRecords.stream() + .map( + record -> { + return RoleDto.builder() + .id(record.getValue("id", Long.class)) + .code(record.getValue("code", String.class)) + .name(record.getValue("name", String.class)) + .isBound( + record.field("is_bound", Boolean.class) != null + ? record.getValue("is_bound", Boolean.class) + : null) + .permissions(record.getValue("permissions", List.class)) + .build(); + }) + .toList(); + return new PageResponseDto<>( + roleRecords.get(0).getValue("total_role", Integer.class), roleDtoList); + } + + public @Nullable RoleDto queryUniqueRoleWithPermission(Long roleId) { + Result roleWithPermissionRecords = roleRepository.fetchUniqueRoleWithPermission(roleId); + if (roleWithPermissionRecords.isEmpty()) { + return null; + } + RoleDto roleDto = createRbacDtoRolePart(roleWithPermissionRecords); + setCurrentRolePermission(roleDto, roleWithPermissionRecords); + return roleDto; + } + + public PageResponseDto> pageQueryPermission( + PageRequestDto pageRequestDto, PermissionQueryDto permissionQueryDto) { + Result permissionRecords = + permissionRepository.pageFetchBy(pageRequestDto, permissionQueryDto); + if (permissionRecords.isEmpty()) { + return PageResponseDto.empty(); + } + List permissionRespDtoList = + permissionRecords.stream() + .map( + record -> + PermissionRespDto.builder() + .id(record.getValue("id", Long.class)) + .name(record.getValue("name", String.class)) + .code(record.getValue("code", String.class)) + .isBound( + record.field("is_bound", Boolean.class) != null + ? record.getValue("is_bound", Boolean.class) + : null) + .build()) + .toList(); + return new PageResponseDto<>( + permissionRecords.get(0).getValue("total_permission", Integer.class), + permissionRespDtoList); + } + + public void bindPermissionBy(Long roleId, List permissionIdList) { + List permissionMapList = + permissionIdList.stream() + .map( + (permissionId -> { + RolePermissionMap rolePermissionMap = new RolePermissionMap(); + rolePermissionMap.setRoleId(roleId); + rolePermissionMap.setPermissionId(permissionId); + return rolePermissionMap; + })) + .collect(Collectors.toList()); + rolePermissionMapRepository.merge(permissionMapList); + } + + public void unBindPermissionBy(Long roleId, List permissionIdList) { + if (CollectionUtils.isEmpty(permissionIdList)) { + return; + } + rolePermissionMapRepository.deleteBy(roleId, permissionIdList); + } + + public void unBindRoleToUser(Long userId, List roleIdList) { + if (CollectionUtils.isEmpty(roleIdList)) { + return; + } + List roles = roleRepository.selectByRoleIdIn(roleIdList); + if (CollectionUtils.isEmpty(roles)) { + throw new BusinessException("unbind role not exist"); + } + userRoleMapRepository.deleteBy(userId, roleIdList); + } + + public void bindRoleToUser(Long userId, List roleIdList) { + List userRoleMapList = + roleIdList.stream() + .map( + (roleId -> { + UserRoleMap userRoleMap = new UserRoleMap(); + userRoleMap.setUserId(userId); + userRoleMap.setRoleId(roleId); + return userRoleMap; + })) + .collect(Collectors.toList()); + userRoleMapRepository.merge(userRoleMapList); + } + + @Transactional(rollbackFor = Throwable.class) + public void bindRoleModuleToUser(Long userId, List eRoleList) { + bindRoleToUser( + userId, + roleRepository + .selectByRoleCodeIn(eRoleList.stream().map(Enum::name).collect(Collectors.toList())) + .stream() + .map(Role::getId) + .toList()); + } + + private void setCurrentRolePermission(RoleDto roleDto, List roleResult) { + if (roleResult.get(0).getValue(PERMISSION.ID) != null) { + roleResult.forEach( + (record) -> { + PermissionRespDto permissionRespDto = createRbacDtoPermissionPart(record); + roleDto.getPermissions().add(permissionRespDto); + }); + } + } + + private PermissionRespDto createRbacDtoPermissionPart(Record record) { + PermissionRespDto permissionRespDto = new PermissionRespDto(); + permissionRespDto.setId(record.getValue(PERMISSION.ID)); + permissionRespDto.setCode(record.getValue(PERMISSION.CODE)); + permissionRespDto.setName(record.getValue(PERMISSION.NAME)); + return permissionRespDto; + } + + private RoleDto createRbacDtoRolePart(List roleResult) { + RoleDto roleDto = new RoleDto(); + roleDto.setId(roleResult.get(0).getValue(ROLE.ID)); + roleDto.setCode(roleResult.get(0).getValue(ROLE.CODE)); + roleDto.setName(roleResult.get(0).getValue(ROLE.NAME)); + return roleDto; + } + + public boolean isRoleDuplicate(String roleCode, String name) { + return roleRepository.fetchOneByCode(roleCode) != null + || roleRepository.fetchOneByName(name) != null; + } + + public boolean isUsernameDuplicate(String username) { + return userRepository.fetchOneByUsername(username) != null; + } + + public boolean isPermissionDuplicate(String code, String name) { + return permissionRepository.fetchOneByCode(code) != null + || permissionRepository.fetchOneByName(name) != null; + } + + @Transactional(rollbackFor = Throwable.class) + public void bindDepartmentBy(DepartmentBindDto departmentBindDto) { + List userDepartmentMaps = + departmentBindDto.departmentIds().stream() + .map( + (departmentId) -> { + UserDepartmentMap userDepartmentMap = new UserDepartmentMap(); + userDepartmentMap.setUserId(departmentBindDto.userId()); + userDepartmentMap.setDepartmentId(departmentId); + return userDepartmentMap; + }) + .toList(); + userDepartmentMapRepository.merge(userDepartmentMaps); + } + + @Transactional(rollbackFor = Throwable.class) + public void unBindDepartmentBy(DepartmentBindDto departmentBindDto) { + for (Long departmentId : departmentBindDto.departmentIds()) { + UserDepartmentMap userDepartmentMap = new UserDepartmentMap(); + userDepartmentMap.setUserId(departmentBindDto.userId()); + userDepartmentMap.setDepartmentId(departmentId); + userDepartmentMapRepository.delete(userDepartmentMap); + } + } + + @Transactional(rollbackFor = Throwable.class) + public void bindPositionBy(PositionBindDto positionBindDto) { + List userPositionMaps = + positionBindDto.positionIds().stream() + .map( + (positionId) -> { + UserPositionMap userPositionMap = new UserPositionMap(); + userPositionMap.setUserId(positionBindDto.userId()); + userPositionMap.setPositionId(positionId); + return userPositionMap; + }) + .toList(); + userPositionMapRepository.merge(userPositionMaps); + } + + @Transactional(rollbackFor = Throwable.class) + public void unBindPositionBy(PositionBindDto positionBindDto) { + for (Long positionId : positionBindDto.positionIds()) { + UserPositionMap userPositionMap = new UserPositionMap(); + userPositionMap.setUserId(positionBindDto.userId()); + userPositionMap.setPositionId(positionId); + userPositionMapRepository.delete(userPositionMap); + } + } +} diff --git a/backend/src/main/java/com/zl/mjga/service/PositionService.java b/backend/src/main/java/com/zl/mjga/service/PositionService.java new file mode 100644 index 0000000..6060e51 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/PositionService.java @@ -0,0 +1,44 @@ +package com.zl.mjga.service; + +import static org.jooq.generated.mjga.tables.Position.POSITION; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.position.PositionQueryDto; +import com.zl.mjga.dto.position.PositionRespDto; +import com.zl.mjga.repository.PositionRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jooq.Record; +import org.jooq.Result; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class PositionService { + + private final PositionRepository positionRepository; + + public PageResponseDto> pageQueryPosition( + PageRequestDto pageRequestDto, PositionQueryDto positionQueryDto) { + Result records = positionRepository.pageFetchBy(pageRequestDto, positionQueryDto); + if (records.isEmpty()) { + return PageResponseDto.empty(); + } + List positions = + records.map( + record -> + PositionRespDto.builder() + .id(record.getValue(POSITION.ID)) + .name(record.getValue(POSITION.NAME)) + .isBound( + record.field("is_bound", Boolean.class) != null + ? record.getValue("is_bound", Boolean.class) + : null) + .build()); + Long totalPosition = records.get(0).getValue("total_position", Long.class); + return new PageResponseDto<>(totalPosition, positions); + } +} diff --git a/backend/src/main/java/com/zl/mjga/service/SchedulerService.java b/backend/src/main/java/com/zl/mjga/service/SchedulerService.java new file mode 100644 index 0000000..711bbeb --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/SchedulerService.java @@ -0,0 +1,103 @@ +package com.zl.mjga.service; + +import static org.jooq.generated.public_.Tables.*; +import static org.quartz.TriggerBuilder.newTrigger; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.scheduler.JobTriggerDto; +import com.zl.mjga.dto.scheduler.QueryDto; +import com.zl.mjga.repository.QrtzJobRepository; +import jakarta.annotation.Resource; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jooq.Record; +import org.jooq.Result; +import org.quartz.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class SchedulerService { + + @Resource(name = "emailJobSchedulerFactory") + private Scheduler emailJobScheduler; + + @Resource(name = "dataBackupSchedulerFactory") + private Scheduler dataBackupScheduler; + + private final QrtzJobRepository qrtzJobRepository; + + public PageResponseDto> getJobWithTriggerBy( + PageRequestDto pageRequestDto, QueryDto queryDto) { + Result records = + qrtzJobRepository.fetchPageWithJobAndTriggerBy(pageRequestDto, queryDto); + if (records.isEmpty()) { + return PageResponseDto.empty(); + } + List jobTriggerDtoList = + records.map( + record -> { + JobTriggerDto jobTriggerDto = new JobTriggerDto(); + jobTriggerDto.setName(record.getValue(QRTZ_JOB_DETAILS.JOB_NAME)); + jobTriggerDto.setGroup(record.getValue(QRTZ_JOB_DETAILS.JOB_GROUP)); + jobTriggerDto.setClassName(record.getValue(QRTZ_JOB_DETAILS.JOB_CLASS_NAME)); + jobTriggerDto.setTriggerName(record.getValue(QRTZ_TRIGGERS.TRIGGER_NAME)); + jobTriggerDto.setTriggerGroup(record.getValue(QRTZ_TRIGGERS.TRIGGER_GROUP)); + jobTriggerDto.setCronExpression(record.getValue(QRTZ_CRON_TRIGGERS.CRON_EXPRESSION)); + jobTriggerDto.setStartTime(record.getValue(QRTZ_TRIGGERS.START_TIME)); + jobTriggerDto.setEndTime(record.getValue(QRTZ_TRIGGERS.END_TIME)); + jobTriggerDto.setNextFireTime(record.getValue(QRTZ_TRIGGERS.NEXT_FIRE_TIME)); + jobTriggerDto.setPreviousFireTime(record.getValue(QRTZ_TRIGGERS.PREV_FIRE_TIME)); + jobTriggerDto.setSchedulerType(record.getValue(QRTZ_TRIGGERS.TRIGGER_TYPE)); + jobTriggerDto.setTriggerState(record.getValue(QRTZ_TRIGGERS.TRIGGER_STATE)); + return jobTriggerDto; + }); + return new PageResponseDto<>( + records.get(0).getValue("total_job", Integer.class), jobTriggerDtoList); + } + + public void resumeTrigger(TriggerKey triggerKey) throws SchedulerException { + emailJobScheduler.resumeTrigger(triggerKey); + dataBackupScheduler.resumeTrigger(triggerKey); + } + + public void pauseTrigger(TriggerKey triggerKey) throws SchedulerException { + emailJobScheduler.pauseTrigger(triggerKey); + dataBackupScheduler.pauseTrigger(triggerKey); + } + + public void triggerJob(JobKey jobKey, Date startAt) throws SchedulerException { + JobDetail jobDetail = emailJobScheduler.getJobDetail(jobKey); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Trigger dayLaterTrigger = + newTrigger() + .withIdentity( + String.format( + "%s-%s-%s", "trigger", authentication.getName(), Instant.now().toEpochMilli()), + "job-management") + .startAt(startAt) + .build(); + emailJobScheduler.scheduleJob(jobDetail, dayLaterTrigger); + } + + public void updateCronTrigger(TriggerKey triggerKey, String cron) throws SchedulerException { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Trigger newTrigger = + TriggerBuilder.newTrigger() + .withIdentity( + String.format( + "%s-%s-%s", + "cronTrigger", authentication.getName(), Instant.now().toEpochMilli()), + "job-management") + .withSchedule(CronScheduleBuilder.cronSchedule(cron)) + .build(); + dataBackupScheduler.rescheduleJob(triggerKey, newTrigger); + } +} diff --git a/backend/src/main/java/com/zl/mjga/service/SignService.java b/backend/src/main/java/com/zl/mjga/service/SignService.java new file mode 100644 index 0000000..8233161 --- /dev/null +++ b/backend/src/main/java/com/zl/mjga/service/SignService.java @@ -0,0 +1,51 @@ +package com.zl.mjga.service; + +import com.zl.mjga.dto.sign.SignInDto; +import com.zl.mjga.dto.sign.SignUpDto; +import com.zl.mjga.exception.BusinessException; +import com.zl.mjga.model.urp.ERole; +import com.zl.mjga.repository.UserRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jooq.generated.mjga.tables.pojos.User; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class SignService { + + private final UserRepository userRepository; + + private final PasswordEncoder passwordEncoder; + + private final IdentityAccessService identityAccessService; + + public Long signIn(SignInDto signInDto) { + User user = userRepository.fetchOneByUsername(signInDto.getUsername()); + if (user == null) { + throw new BusinessException(String.format("%s user not found", signInDto.getUsername())); + } + if (!passwordEncoder.matches(signInDto.getPassword(), user.getPassword())) { + throw new BusinessException("password invalid"); + } + return user.getId(); + } + + @Transactional(rollbackFor = Throwable.class) + public void signUp(SignUpDto signUpDto) { + if (identityAccessService.isUsernameDuplicate(signUpDto.getUsername())) { + throw new BusinessException( + String.format("username %s already exist", signUpDto.getUsername())); + } + User user = new User(); + user.setUsername(signUpDto.getUsername()); + user.setPassword(passwordEncoder.encode(signUpDto.getPassword())); + userRepository.insert(user); + User insertUser = userRepository.fetchOneByUsername(signUpDto.getUsername()); + identityAccessService.bindRoleModuleToUser(insertUser.getId(), List.of(ERole.GENERAL)); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..56c3477 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,31 @@ +server: + port: 8080 +logging: + file: + path: /var/log + level: + org: + springframework: + security: debug + flywaydb: debug + jooq: debug +cors: + allowedOrigins: ${ALLOWED_ORIGINS} + allowedMethods: ${ALLOWED_METHODS} + allowedHeaders: ${ALLOWED_HEADERS} + allowedExposeHeaders: ${ALLOWED_EXPOSE_HEADERS} +spring: + datasource: + url: jdbc:postgresql://${DATABASE_HOST_PORT}/${DATABASE_DB} + username: ${DATABASE_USER} + password: ${DATABASE_PASSWORD} + flyway: + enabled: true + locations: classpath:db/migration + default-schema: ${DATABASE_DEFAULT_SCHEMA} +springdoc: + swagger-ui: + path: /swagger-ui.html +jwt: + secret: ${JWT_SECRET:secret} + expiration-min: ${JWT_EXPIRATION_MIN:100} diff --git a/backend/src/main/resources/db/migration/V1_0_0__init_table.sql b/backend/src/main/resources/db/migration/V1_0_0__init_table.sql new file mode 100644 index 0000000..2cef2f3 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1_0_0__init_table.sql @@ -0,0 +1,67 @@ +CREATE SCHEMA IF NOT EXISTS mjga; + +CREATE TABLE mjga.user ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR NOT NULL UNIQUE, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + password VARCHAR NOT NULL, + enable BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE mjga.permission ( + id BIGSERIAL PRIMARY KEY, + code VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL UNIQUE +); + +CREATE TABLE mjga.role ( + id BIGSERIAL PRIMARY KEY, + code VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL UNIQUE +); + +CREATE TABLE mjga.role_permission_map ( + role_id BIGINT NOT NULL, + permission_id BIGINT NOT NULL, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES mjga.role(id) ON DELETE RESTRICT, + FOREIGN KEY (permission_id) REFERENCES mjga.permission(id) ON DELETE RESTRICT +); + +CREATE TABLE mjga.user_role_map ( + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE RESTRICT, + FOREIGN KEY (role_id) REFERENCES mjga.role(id) ON DELETE RESTRICT +); + +CREATE TABLE mjga.department ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + parent_id BIGINT, + FOREIGN KEY (parent_id) + REFERENCES mjga.department(id) + ON DELETE RESTRICT +); + +CREATE TABLE mjga.user_department_map ( + user_id BIGINT NOT NULL, + department_id BIGINT NOT NULL, + PRIMARY KEY (user_id, department_id), + FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON UPDATE NO ACTION ON DELETE RESTRICT, + FOREIGN KEY (department_id) REFERENCES mjga.department(id) ON UPDATE NO ACTION ON DELETE RESTRICT +); + +CREATE TABLE mjga.position ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE +); + +CREATE TABLE mjga.user_position_map ( + user_id BIGINT NOT NULL, + position_id BIGINT NOT NULL, + PRIMARY KEY (user_id, position_id), + FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON UPDATE NO ACTION ON DELETE RESTRICT, + FOREIGN KEY (position_id) REFERENCES mjga.position(id) ON UPDATE NO ACTION ON DELETE RESTRICT +); \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1_0_1__insert_init_table.sql b/backend/src/main/resources/db/migration/V1_0_1__insert_init_table.sql new file mode 100644 index 0000000..81a5139 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1_0_1__insert_init_table.sql @@ -0,0 +1,29 @@ +INSERT INTO mjga.user (username, password) +VALUES ('admin', '$2a$10$7zfEdqQYJrBnmDdu7UkgS.zOAsJf4bB1ZYrVhCBAIvIoPbEmeVnVe'); + +INSERT INTO mjga.role (code, name) +VALUES ('ADMIN', 'ADMIN'), + ('GENERAL', 'GENERAL'); + +INSERT INTO mjga.permission (code, name) +VALUES ('READ_POSITION_PERMISSION', 'READ_POSITION_PERMISSION'), + ('WRITE_POSITION_PERMISSION', 'WRITE_POSITION_PERMISSION'), + ('READ_DEPARTMENT_PERMISSION', 'READ_DEPARTMENT_PERMISSION'), + ('WRITE_DEPARTMENT_PERMISSION', 'WRITE_DEPARTMENT_PERMISSION'), + ('READ_SCHEDULER_PERMISSION', 'READ_SCHEDULER_PERMISSION'), + ('WRITE_SCHEDULER_PERMISSION', 'WRITE_SCHEDULER_PERMISSION'), + ('WRITE_USER_ROLE_PERMISSION', 'WRITE_USER_ROLE_PERMISSION'), + ('READ_USER_ROLE_PERMISSION', 'READ_USER_ROLE_PERMISSION'); + +INSERT INTO mjga.user_role_map (user_id, role_id) +VALUES (1, 1); + +INSERT INTO mjga.role_permission_map (role_id, permission_id) +VALUES (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (1, 6), + (1, 7), + (1, 8); diff --git a/backend/src/main/resources/db/migration/V1_0_2__init_quartz.sql b/backend/src/main/resources/db/migration/V1_0_2__init_quartz.sql new file mode 100644 index 0000000..bc08e12 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1_0_2__init_quartz.sql @@ -0,0 +1,194 @@ +CREATE SCHEMA IF NOT EXISTS public; + +SET search_path TO public; + +CREATE TABLE QRTZ_JOB_DETAILS +( + SCHED_NAME VARCHAR(120) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + JOB_CLASS_NAME VARCHAR(250) NOT NULL, + IS_DURABLE BOOL NOT NULL, + IS_NONCONCURRENT BOOL NOT NULL, + IS_UPDATE_DATA BOOL NOT NULL, + REQUESTS_RECOVERY BOOL NOT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) +); + +CREATE TABLE QRTZ_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + NEXT_FIRE_TIME BIGINT NULL, + PREV_FIRE_TIME BIGINT NULL, + PRIORITY INTEGER NULL, + TRIGGER_STATE VARCHAR(16) NOT NULL, + TRIGGER_TYPE VARCHAR(8) NOT NULL, + START_TIME BIGINT NOT NULL, + END_TIME BIGINT NULL, + CALENDAR_NAME VARCHAR(200) NULL, + MISFIRE_INSTR SMALLINT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) + REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP) +); + +CREATE TABLE QRTZ_SIMPLE_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + REPEAT_COUNT BIGINT NOT NULL, + REPEAT_INTERVAL BIGINT NOT NULL, + TIMES_TRIGGERED BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_CRON_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + CRON_EXPRESSION VARCHAR(120) NOT NULL, + TIME_ZONE_ID VARCHAR(80), + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_SIMPROP_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + STR_PROP_1 VARCHAR(512) NULL, + STR_PROP_2 VARCHAR(512) NULL, + STR_PROP_3 VARCHAR(512) NULL, + INT_PROP_1 INT NULL, + INT_PROP_2 INT NULL, + LONG_PROP_1 BIGINT NULL, + LONG_PROP_2 BIGINT NULL, + DEC_PROP_1 NUMERIC(13, 4) NULL, + DEC_PROP_2 NUMERIC(13, 4) NULL, + BOOL_PROP_1 BOOL NULL, + BOOL_PROP_2 BOOL NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_BLOB_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + BLOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_CALENDARS +( + SCHED_NAME VARCHAR(120) NOT NULL, + CALENDAR_NAME VARCHAR(200) NOT NULL, + CALENDAR BYTEA NOT NULL, + PRIMARY KEY (SCHED_NAME, CALENDAR_NAME) +); + + +CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_FIRED_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + ENTRY_ID VARCHAR(95) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + INSTANCE_NAME VARCHAR(200) NOT NULL, + FIRED_TIME BIGINT NOT NULL, + SCHED_TIME BIGINT NOT NULL, + PRIORITY INTEGER NOT NULL, + STATE VARCHAR(16) NOT NULL, + JOB_NAME VARCHAR(200) NULL, + JOB_GROUP VARCHAR(200) NULL, + IS_NONCONCURRENT BOOL NULL, + REQUESTS_RECOVERY BOOL NULL, + PRIMARY KEY (SCHED_NAME, ENTRY_ID) +); + +CREATE TABLE QRTZ_SCHEDULER_STATE +( + SCHED_NAME VARCHAR(120) NOT NULL, + INSTANCE_NAME VARCHAR(200) NOT NULL, + LAST_CHECKIN_TIME BIGINT NOT NULL, + CHECKIN_INTERVAL BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, INSTANCE_NAME) +); + +CREATE TABLE QRTZ_LOCKS +( + SCHED_NAME VARCHAR(120) NOT NULL, + LOCK_NAME VARCHAR(40) NOT NULL, + PRIMARY KEY (SCHED_NAME, LOCK_NAME) +); + +CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY + ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY); +CREATE INDEX IDX_QRTZ_J_GRP + ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP); + +CREATE INDEX IDX_QRTZ_T_J + ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_T_JG + ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_T_C + ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME); +CREATE INDEX IDX_QRTZ_T_G + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); +CREATE INDEX IDX_QRTZ_T_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_N_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_N_G_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME + ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_ST + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE); + +CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME); +CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY); +CREATE INDEX IDX_QRTZ_FT_J_G + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_FT_JG + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_FT_T_G + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP); +CREATE INDEX IDX_QRTZ_FT_TG + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); + + +COMMIT; diff --git a/backend/src/test/java/com/zl/mjga/integration/cache/CacheTest.java b/backend/src/test/java/com/zl/mjga/integration/cache/CacheTest.java new file mode 100644 index 0000000..a8c752d --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/cache/CacheTest.java @@ -0,0 +1,51 @@ +package com.zl.mjga.integration.cache; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.zl.mjga.config.cache.CacheConfig; +import com.zl.mjga.service.CacheService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +@SpringJUnitConfig(classes = {CacheConfig.class, CacheService.class}) +@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class}) +public class CacheTest { + + @Autowired private CacheService cacheService; + + @Test + void + getVerifyCodeBy_upsertVerifyCodeBy_whenSetCacheValue_subsequentGetCacheShouldReturnUpdatedValue() { + cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216"); + String verifyCode = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); + assertThat(verifyCode).isEqualTo("ej1x8T4XiluV8D216"); + } + + @Test + void removeVerifyCodeBy_whenRemoveCacheValue_subsequentGetCacheShouldReturnNull() { + cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216"); + String verifyCode = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); + cacheService.removeVerifyCodeBy("WsxOtE0d6Vc1glZ"); + String verifyCode2 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); + assertThat(verifyCode).isEqualTo("ej1x8T4XiluV8D216"); + assertThat(verifyCode2).isNull(); + } + + @Test + void clearAllVerifyCode_whenCleanCache_subsequentGetCacheShouldReturnNewValue() { + cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216"); + cacheService.upsertVerifyCodeBy("hNYcK0MDjX4197", "Ll1v93jiXwHLji"); + String verifyCode1 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); + String verifyCode2 = cacheService.getVerifyCodeBy("hNYcK0MDjX4197"); + cacheService.clearAllVerifyCode(); + String verifyCode3 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ"); + String verifyCode4 = cacheService.getVerifyCodeBy("hNYcK0MDjX4197"); + assertThat(verifyCode1).isEqualTo("ej1x8T4XiluV8D216"); + assertThat(verifyCode2).isEqualTo("Ll1v93jiXwHLji"); + assertThat(verifyCode3).isNull(); + assertThat(verifyCode4).isNull(); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/e2e/SignE2ETest.java b/backend/src/test/java/com/zl/mjga/integration/e2e/SignE2ETest.java new file mode 100644 index 0000000..aec02e6 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/e2e/SignE2ETest.java @@ -0,0 +1,121 @@ +package com.zl.mjga.integration.e2e; + +import com.zl.mjga.repository.UserRepository; +import com.zl.mjga.repository.UserRoleMapRepository; +import java.time.Duration; +import org.jooq.generated.mjga.tables.pojos.User; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureWebTestClient +@Disabled +public class SignE2ETest { + + @Value("${jwt.cookie-name}") + private String jwtCookieName; + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration-min}") + private int expirationMin; + + @Autowired private WebTestClient webTestClient; + + @Autowired private UserRepository userRepository; + + @Autowired private UserRoleMapRepository userRoleMapRepository; + + @Autowired private PasswordEncoder passwordEncoder; + + @Autowired private TestRestTemplate testRestTemplate; + + @AfterEach + void cleanUp() { + User user = userRepository.fetchOneByUsername("test_5fab32c22a3e"); + userRoleMapRepository.deleteByUserId(user.getId()); + userRepository.deleteByUsername("test_5fab32c22a3e"); + } + + @Test + void signUp() { + webTestClient + .post() + .uri("/auth/sign-up") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue( + """ +{ + "username": "test_5fab32c22a3e", + "password": "test_eab28b939ba1" +} +""") + .exchange() + .expectStatus() + .isCreated(); + } + + @Test + void signIn() { + User stubUser = new User(); + stubUser.setUsername("test_5fab32c22a3e"); + stubUser.setPassword(passwordEncoder.encode("test_eab28b939ba1")); + userRepository.insert(stubUser); + webTestClient + .post() + .uri("/auth/sign-in") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue( + """ +{ + "username": "test_5fab32c22a3e", + "password": "test_eab28b939ba1" +} +""") + .exchange() + .expectCookie() + .exists(jwtCookieName) + .expectCookie() + .maxAge(jwtCookieName, Duration.ofSeconds(expirationMin * 60L)) + .expectStatus() + .isOk(); + } + + @Test + void signOut() { + User stubUser = new User(); + stubUser.setUsername("test_5fab32c22a3e"); + stubUser.setPassword(passwordEncoder.encode("test_eab28b939ba1")); + userRepository.insert(stubUser); + User loginUser = new User(); + loginUser.setUsername("test_5fab32c22a3e"); + loginUser.setPassword("test_eab28b939ba1"); + HttpHeaders headers = + testRestTemplate.postForEntity("/auth/sign-in", loginUser, String.class).getHeaders(); + headers + .get("Set-Cookie") + .forEach( + cookie -> { + if (cookie.startsWith(jwtCookieName)) { + webTestClient + .post() + .uri("/auth/sign-out") + .header("Cookie", cookie) + .exchange() + .expectCookie() + .maxAge(jwtCookieName, Duration.ofSeconds(0L)) + .expectStatus() + .isOk(); + } + }); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java b/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java new file mode 100644 index 0000000..8532db4 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/mvc/JacksonAnnotationMvcTest.java @@ -0,0 +1,78 @@ +package com.zl.mjga.integration.mvc; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.zl.mjga.config.security.HttpFireWallConfig; +import com.zl.mjga.controller.IdentityAccessController; +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.urp.UserQueryDto; +import com.zl.mjga.dto.urp.UserRolePermissionDto; +import com.zl.mjga.repository.PermissionRepository; +import com.zl.mjga.repository.RoleRepository; +import com.zl.mjga.repository.UserRepository; +import com.zl.mjga.service.IdentityAccessService; +import java.time.OffsetDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(value = {IdentityAccessController.class}) +@Import({HttpFireWallConfig.class}) +public class JacksonAnnotationMvcTest { + + @MockBean private IdentityAccessService identityAccessService; + @Autowired private MockMvc mockMvc; + @MockBean private UserRepository userRepository; + @MockBean private RoleRepository roleRepository; + @MockBean private PermissionRepository permissionRepository; + + @Test + @WithMockUser + void fieldWithJsonWriteOnlyAnnotation_whenResponseIncludeField_responseJsonShouldNotExist() + throws Exception { + String stubUsername = "test_04cb017e1fe6"; + String stubPassword = "y1hxAC0V0e4B3s8sJ"; + UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto(); + stubUserRolePermissionDto.setId(1L); + stubUserRolePermissionDto.setUsername(stubUsername); + stubUserRolePermissionDto.setPassword(stubPassword); + when(identityAccessService.pageQueryUser( + PageRequestDto.of(1, 5), new UserQueryDto(stubUsername))) + .thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto))); + mockMvc + .perform( + get(String.format("/iam/users?page=1&size=5&username=%s", stubUsername)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].username").value(stubUsername)) + .andExpect(jsonPath("$.data[0].password").doesNotExist()); + } + + @Test + @WithMockUser + void dateFieldWithFormatAnnotation_whenResponseIncludeField_fieldShouldBeExpectDataFormat() + throws Exception { + OffsetDateTime stubCreateDateTime = + OffsetDateTime.of(2023, 12, 2, 1, 1, 1, 0, OffsetDateTime.now().getOffset()); + UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto(); + stubUserRolePermissionDto.setCreateTime(stubCreateDateTime); + when(identityAccessService.pageQueryUser(any(PageRequestDto.class), any(UserQueryDto.class))) + .thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto))); + mockMvc + .perform( + get(String.format("/iam/users?page=1&size=5&username=%s", "7bF3mcNVTj6P6v2")) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].createTime").value("2023-12-02 01:01:01")); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/mvc/SignMvcTest.java b/backend/src/test/java/com/zl/mjga/integration/mvc/SignMvcTest.java new file mode 100644 index 0000000..5b840ab --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/mvc/SignMvcTest.java @@ -0,0 +1,144 @@ +package com.zl.mjga.integration.mvc; + +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.zl.mjga.config.security.HttpFireWallConfig; +import com.zl.mjga.config.security.Jwt; +import com.zl.mjga.controller.SignController; +import com.zl.mjga.dto.sign.SignInDto; +import com.zl.mjga.service.SignService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(value = {SignController.class}) +@Import({HttpFireWallConfig.class}) +class SignMvcTest { + + @MockBean private SignService signService; + + @MockBean private Jwt jwt; + + @Autowired private MockMvc mockMvc; + + @Test + @WithMockUser + void signIn_givenValidHttpRequest_shouldSucceedWith200() throws Exception { + String stubUsername = "test_04cb017e1fe6"; + String stubPassword = "test_567472858b8c"; + SignInDto signInDto = new SignInDto(); + signInDto.setUsername(stubUsername); + signInDto.setPassword(stubPassword); + + when(signService.signIn(signInDto)).thenReturn(1L); + mockMvc + .perform( + post("/auth/sign-in") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "test_04cb017e1fe6", + "password": "test_567472858b8c" + } + """) + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void signIn_givenInValidHttpRequest_shouldFailedWith400() throws Exception { + String stubUsername = "test_04cb017e1fe6"; + String stubPassword = "test_567472858b8c"; + SignInDto signInDto = new SignInDto(); + signInDto.setUsername(stubUsername); + signInDto.setPassword(stubPassword); + + when(signService.signIn(signInDto)).thenReturn(1L); + mockMvc + .perform( + post("/auth/sign-in") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content( + """ + { + "username": "test_04cb017e1fe6", + "password": "test_567472858b8c" + } + """) + .with(csrf())) + .andExpect(status().isBadRequest()); + + when(signService.signIn(signInDto)).thenReturn(1L); + mockMvc + .perform( + post("/auth/sign-in") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "test_04cb017e1fe6" + } + """) + .with(csrf())) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser + void signUp_givenValidHttpRequest_shouldSucceedWith200() throws Exception { + mockMvc + .perform( + post("/auth/sign-up") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "test_04cb017e1fe6", + "password": "test_567472858b8c" + } + """) + .with(csrf())) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser + void signUp_givenInValidHttpRequest_shouldFailedWith400() throws Exception { + mockMvc + .perform( + post("/auth/sign-up") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content( + """ + { + "username": "test_04cb017e1fe6", + "password": "test_567472858b8c" + } + """) + .with(csrf())) + .andExpect(status().isBadRequest()); + + mockMvc + .perform( + post("/auth/sign-up") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "test_04cb017e1fe6" + } + """) + .with(csrf())) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/mvc/UserRolePermissionMvcTest.java b/backend/src/test/java/com/zl/mjga/integration/mvc/UserRolePermissionMvcTest.java new file mode 100644 index 0000000..6006de1 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/mvc/UserRolePermissionMvcTest.java @@ -0,0 +1,225 @@ +package com.zl.mjga.integration.mvc; + +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zl.mjga.config.security.HttpFireWallConfig; +import com.zl.mjga.controller.IdentityAccessController; +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.urp.*; +import com.zl.mjga.repository.PermissionRepository; +import com.zl.mjga.repository.RoleRepository; +import com.zl.mjga.repository.UserRepository; +import com.zl.mjga.service.IdentityAccessService; +import java.util.List; +import org.jooq.generated.mjga.tables.pojos.User; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(value = {IdentityAccessController.class}) +@Import({HttpFireWallConfig.class}) +class UserRolePermissionMvcTest { + + @MockBean private IdentityAccessService identityAccessService; + @Autowired private MockMvc mockMvc; + @MockBean private UserRepository userRepository; + @MockBean private RoleRepository roleRepository; + @MockBean private PermissionRepository permissionRepository; + + @Test + @WithMockUser + void currentUser_givenValidHttpRequest_shouldSucceedWith200AndReturnJson() throws Exception { + String stubUsername = "test_04cb017e1fe6"; + UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto(); + stubUserRolePermissionDto.setId(1L); + stubUserRolePermissionDto.setUsername(stubUsername); + User stubUser = new User(); + stubUser.setId(1L); + when(userRepository.fetchOneByUsername(anyString())).thenReturn(stubUser); + when(identityAccessService.queryUniqueUserWithRolePermission(anyLong())) + .thenReturn(stubUserRolePermissionDto); + mockMvc + .perform(get("/iam/me")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value(stubUsername)); + } + + @Test + @WithMockUser + void deleteUser_givenValidHttpRequest_shouldSucceedWith200() throws Exception { + Long stubUserId = 1L; + mockMvc + .perform( + delete(String.format("/iam/user?userId=%s", stubUserId)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void upsertUser_givenValidHttpRequest_shouldSucceedWith200() throws Exception { + UserUpsertDto userUpsertDto = new UserUpsertDto(); + userUpsertDto.setUsername("username"); + userUpsertDto.setPassword("password"); + userUpsertDto.setEnable(true); + + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writeValueAsString(userUpsertDto); + + mockMvc + .perform( + post("/iam/user").contentType(MediaType.APPLICATION_JSON).content(json).with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void upsertRole_givenValidHttpRequest_shouldSucceedWith200() throws Exception { + RoleUpsertDto roleUpsertDto = new RoleUpsertDto(); + roleUpsertDto.setCode("roleCode"); + roleUpsertDto.setName("name"); + + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writeValueAsString(roleUpsertDto); + + mockMvc + .perform( + post("/iam/role").contentType(MediaType.APPLICATION_JSON).content(json).with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void deleteRole_givenValidHttpRequest_shouldSucceedWith200() throws Exception { + Long stubRoleId = 1L; + mockMvc + .perform( + delete(String.format("/iam/role?roleId=%s", stubRoleId)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void upsertPermission_givenValidHttpRequest_shouldSucceedWith200() throws Exception { + PermissionUpsertDto permissionUpsertDto = new PermissionUpsertDto(); + permissionUpsertDto.setCode("roleCode"); + permissionUpsertDto.setName("name"); + + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writeValueAsString(permissionUpsertDto); + + mockMvc + .perform( + post("/iam/permission") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void deletePermission_givenValidHttpRequest_shouldSucceedWith200() throws Exception { + Long permissionId = 1L; + mockMvc + .perform( + delete(String.format("/iam/permission?permissionId=%s", permissionId)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser + void pageQueryUser_givenValidHttpRequest_shouldSucceedWith200AndReturnJson() throws Exception { + String stubUsername = "test_04cb017e1fe6"; + UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto(); + stubUserRolePermissionDto.setId(1L); + stubUserRolePermissionDto.setUsername(stubUsername); + when(identityAccessService.pageQueryUser( + PageRequestDto.of(1, 5), new UserQueryDto(stubUsername))) + .thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto))); + mockMvc + .perform( + get(String.format("/iam/users?page=1&size=5&username=%s", stubUsername)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].username").value(stubUsername)); + } + + @Test + @WithMockUser + void pageQueryRole_givenValidHttpRequest_shouldSucceedWith200AndReturnJson() throws Exception { + Long stubUserId = 1L; + Long stubRoleId = 1L; + String stubRoleCode = "UZ1Ej9vx5y8L4"; + String stubRoleName = "B90KM9Pw2ZH9P8OAS"; + RoleQueryDto stubRoleQueryDto = new RoleQueryDto(); + stubRoleQueryDto.setUserId(stubUserId); + stubRoleQueryDto.setRoleId(stubRoleId); + stubRoleQueryDto.setRoleCode(stubRoleCode); + stubRoleQueryDto.setRoleName(stubRoleName); + RoleDto stubRoleDto = new RoleDto(); + stubRoleDto.setId(1L); + stubRoleDto.setName(stubRoleName); + stubRoleDto.setCode(stubRoleCode); + stubRoleDto.setPermissions( + List.of(new PermissionRespDto(1L, "9VWU1nmU89zEVH", "9VWU1nmU89zEVH", false))); + when(identityAccessService.pageQueryRole(PageRequestDto.of(1, 5), stubRoleQueryDto)) + .thenReturn(new PageResponseDto<>(1, List.of(stubRoleDto))); + + mockMvc + .perform( + get(String.format( + "/iam/roles?page=1&size=5&userId=%s&roleId=%s&roleCode=%s&roleName=%s", + stubUserId, stubRoleId, stubRoleCode, stubRoleName)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].name").value(stubRoleName)); + } + + @Test + @WithMockUser + void pageQueryPermission_givenValidHttpRequest_shouldSucceedWith200AndReturnJson() + throws Exception { + Long stubRoleId = 1L; + Long stubPermissionId = 1L; + String stubPermissionCode = "UZ1Ej9vx5y8L4"; + String stubPermissionName = "B90KM9Pw2ZH9P8OAS"; + PermissionQueryDto stubPermissionQueryDto = new PermissionQueryDto(); + stubPermissionQueryDto.setRoleId(stubRoleId); + stubPermissionQueryDto.setPermissionId(stubPermissionId); + stubPermissionQueryDto.setPermissionCode(stubPermissionCode); + stubPermissionQueryDto.setPermissionName(stubPermissionName); + + PermissionRespDto stubPermissionRespDto = new PermissionRespDto(); + stubPermissionRespDto.setId(stubPermissionId); + stubPermissionRespDto.setName(stubPermissionName); + stubPermissionRespDto.setCode(stubPermissionCode); + when(identityAccessService.pageQueryPermission(PageRequestDto.of(1, 5), stubPermissionQueryDto)) + .thenReturn(new PageResponseDto<>(1, List.of(stubPermissionRespDto))); + + mockMvc + .perform( + get(String.format( + "/iam/permissions?page=1&size=5&roleId=%s&permissionId=%s&permissionCode=%s&permissionName=%s", + stubRoleId, stubPermissionId, stubPermissionCode, stubPermissionName)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].name").value(stubPermissionName)); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/persistence/AbstractDataAccessLayerTest.java b/backend/src/test/java/com/zl/mjga/integration/persistence/AbstractDataAccessLayerTest.java new file mode 100644 index 0000000..766182a --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/persistence/AbstractDataAccessLayerTest.java @@ -0,0 +1,33 @@ +package com.zl.mjga.integration.persistence; + +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jooq.JooqTest; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ComponentScans; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +@JooqTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ComponentScans({@ComponentScan("jooq.tables.daos"), @ComponentScan("com.zl.mjga.repository")}) +@Testcontainers +public class AbstractDataAccessLayerTest { + + public static PostgreSQLContainer postgres = + new PostgreSQLContainer<>("postgres:17.3-alpine").withDatabaseName("mjga"); + + @DynamicPropertySource + static void postgresProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgres::getJdbcUrl); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + registry.add("spring.flyway.locations", () -> "classpath:db/migration/test"); + registry.add("spring.flyway.default-schema", () -> "public"); + } + + static { + postgres.start(); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/persistence/JooqTutorialsTest.java b/backend/src/test/java/com/zl/mjga/integration/persistence/JooqTutorialsTest.java new file mode 100644 index 0000000..750ab38 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/persistence/JooqTutorialsTest.java @@ -0,0 +1,160 @@ +package com.zl.mjga.integration.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.jooq.generated.mjga.tables.User.USER; +import static org.jooq.impl.DSL.asterisk; + +import java.util.List; +import org.jooq.DSLContext; +import org.jooq.Record; +import org.jooq.Result; +import org.jooq.generated.mjga.tables.pojos.User; +import org.jooq.generated.mjga.tables.records.UserRecord; +import org.jooq.impl.DSL; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +public class JooqTutorialsTest extends AbstractDataAccessLayerTest { + + @Autowired private DSLContext dsl; + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','lbHHwHjTzpOiRHTs')" + }) + void queryWithDsl() { + List users = dsl.selectFrom(USER).fetchInto(User.class); + assertThat(users.size()).isEqualTo(2); + assertThat(users.get(0).getUsername()).isEqualTo("testUserA"); + assertThat(users.get(1).getUsername()).isEqualTo("testUserB"); + } + + @Test + void insertWithOrmFeel() { + UserRecord userRecord = dsl.newRecord(USER); + userRecord.setUsername("9hrb5Fv@gmail.com"); + userRecord.setPassword("falr2b9nCVY5hS1o"); + userRecord.store(); + UserRecord fetchedOne = dsl.fetchOne(USER, USER.USERNAME.eq("9hrb5Fv@gmail.com")); + assertThat(fetchedOne.getPassword()).isEqualTo("falr2b9nCVY5hS1o"); + } + + @Test + void updateWithOrmFeel() { + UserRecord userRecord = dsl.newRecord(USER); + userRecord.setUsername("9hrb5Fv@gmail.com"); + userRecord.setPassword("falr2b9nCVY5hS1o"); + userRecord.store(); + + UserRecord fetchedOne = dsl.fetchOne(USER, USER.USERNAME.eq("9hrb5Fv@gmail.com")); + assertThat(fetchedOne.getPassword()).isEqualTo("falr2b9nCVY5hS1o"); + + userRecord.setPassword("JHMDoQPKuEcgILE6"); + userRecord.store(); + + fetchedOne.refresh(); + assertThat(fetchedOne.getPassword()).isEqualTo("JHMDoQPKuEcgILE6"); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','lbHHwHjTzpOiRHTs')" + }) + void deleteWithOrmFeel() { + UserRecord userRecord1 = dsl.fetchOne(USER, USER.USERNAME.eq("testUserA")); + assertThat(userRecord1.get(USER.USERNAME)).isEqualTo("testUserA"); + userRecord1.delete(); + UserRecord userRecord2 = dsl.fetchOne(USER, USER.USERNAME.eq("testUserA")); + assertThat(userRecord2).isNull(); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','lbHHwHjTzpOiRHTs')", + "INSERT INTO mjga.user (id, username, password) VALUES (3, 'testUserC','yF25WscLYmA8')", + "INSERT INTO mjga.user (id, username, password) VALUES (4, 'testUserD','yF25WscLYmA8')", + "INSERT INTO mjga.user (id, username, password) VALUES (5, 'testUserE','x60FelJjyd0B')" + }) + void pagingQuery() { + List users = + dsl.select(USER.USERNAME).from(USER).limit(2).offset(1).fetchInto(User.class); + assertThat(users.size()).isEqualTo(2); + assertThat(users.get(0).getUsername()).isEqualTo("testUserB"); + assertThat(users.get(1).getUsername()).isEqualTo("testUserC"); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','lbHHwHjTzpOiRHTs')", + "INSERT INTO mjga.user (id, username, password) VALUES (3, 'testUserC','yF25WscLYmA8')", + "INSERT INTO mjga.user (id, username, password) VALUES (4, 'testUserD','yF25WscLYmA8')", + "INSERT INTO mjga.user (id, username, password) VALUES (5, 'testUserE','x60FelJjyd0B')" + }) + void pagingSortQuery() { + List users = + dsl.select(USER.USERNAME) + .from(USER) + .orderBy(USER.ID.desc()) + .limit(3) + .offset(1) + .fetchInto(User.class); + assertThat(users.size()).isEqualTo(3); + assertThat(users.get(0).getUsername()).isEqualTo("testUserD"); + assertThat(users.get(1).getUsername()).isEqualTo("testUserC"); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','a')", + "INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','b')", + "INSERT INTO mjga.user (id, username, password) VALUES (3, 'testUserC','c')", + "INSERT INTO mjga.user (id, username, password) VALUES (4, 'testUserD','c')", + "INSERT INTO mjga.user (id, username, password) VALUES (5, 'testUserE','c')" + }) + void fetchAndTiesQuery() { + List users = + dsl.select(USER.USERNAME) + .from(USER) + .orderBy(USER.PASSWORD.asc()) + .limit(3) + .withTies() + .offset(0) + .fetchInto(User.class); + assertThat(users.size()).isEqualTo(5); + assertThat(users.get(0).getUsername()).isEqualTo("testUserA"); + assertThat(users.get(4).getUsername()).isEqualTo("testUserE"); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','a')", + "INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','b')", + "INSERT INTO mjga.user (id, username, password) VALUES (3, 'testUserC','c')", + "INSERT INTO mjga.user (id, username, password) VALUES (4, 'testUserD','e')", + "INSERT INTO mjga.user (id, username, password) VALUES (5, 'testUserE','f')" + }) + void windowFunctionQuery() { + Result resultWithWindow = + dsl.select(asterisk(), DSL.count().over().as("total_user")) + .from(USER) + .orderBy(USER.ID.asc()) + .limit(4) + .offset(0) + .fetch(); + assertThat(resultWithWindow.size()).isEqualTo(4); + assertThat(resultWithWindow.get(0).getValue("total_user")).isEqualTo(5); + assertThat(resultWithWindow.get(0).getValue(USER.USERNAME)).isEqualTo("testUserA"); + assertThat(resultWithWindow.get(1).getValue(USER.USERNAME)).isEqualTo("testUserB"); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/persistence/SortByDALTest.java b/backend/src/test/java/com/zl/mjga/integration/persistence/SortByDALTest.java new file mode 100644 index 0000000..536f6fc --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/persistence/SortByDALTest.java @@ -0,0 +1,71 @@ +package com.zl.mjga.integration.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.jooq.generated.mjga.tables.User.USER; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.urp.UserQueryDto; +import com.zl.mjga.repository.*; +import java.util.HashMap; +import org.jooq.Record; +import org.jooq.Result; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +public class SortByDALTest extends AbstractDataAccessLayerTest { + @Autowired private UserRoleMapRepository userRoleMapRepository; + + @Autowired private RolePermissionMapRepository rolePermissionMapRepository; + + @Autowired private UserRepository userRepository; + + @Autowired private RoleRepository roleRepository; + + @Autowired private PermissionRepository permissionRepository; + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.user (id, username,password) VALUES (2, 'testB','NTjRCeUq2EqCy')", + "INSERT INTO mjga.user (id, username,password) VALUES (3, 'testC','qFVVFvPqs291k10')", + }) + void userPageFetchWithNoSort() { + UserQueryDto rbacQueryDto = new UserQueryDto("test"); + Result records = userRepository.pageFetchBy(PageRequestDto.of(1, 10), rbacQueryDto); + assertThat(records.get(0).get(USER.ID)).isEqualTo(1); + assertThat(records.get(1).get(USER.ID)).isEqualTo(2); + assertThat(records.get(2).get(USER.ID)).isEqualTo(3); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testA','1')", + "INSERT INTO mjga.user (id, username,password) VALUES (2, 'testB','2')", + "INSERT INTO mjga.user (id, username,password) VALUES (3, 'testC','3')", + "INSERT INTO mjga.user (id, username,password) VALUES (4, 'testD','3')", + }) + void userPageFetchWithSort() { + UserQueryDto rbacQueryDto = new UserQueryDto("test"); + HashMap sortByIdDesc = new HashMap<>(); + sortByIdDesc.put("id", PageRequestDto.Direction.DESC); + Result records = + userRepository.pageFetchBy(PageRequestDto.of(1, 10, sortByIdDesc), rbacQueryDto); + assertThat(records.get(0).get(USER.ID)).isEqualTo(4); + assertThat(records.get(1).get(USER.ID)).isEqualTo(3); + assertThat(records.get(2).get(USER.ID)).isEqualTo(2); + assertThat(records.get(3).get(USER.ID)).isEqualTo(1); + + HashMap sortByPasswordAndId = new HashMap<>(); + sortByPasswordAndId.put("password", PageRequestDto.Direction.DESC); + sortByIdDesc.put("id", PageRequestDto.Direction.ASC); + Result records2 = + userRepository.pageFetchBy(PageRequestDto.of(1, 10, sortByPasswordAndId), rbacQueryDto); + assertThat(records2.get(0).get(USER.ID)).isEqualTo(3); + assertThat(records2.get(1).get(USER.ID)).isEqualTo(4); + assertThat(records2.get(2).get(USER.ID)).isEqualTo(2); + assertThat(records2.get(3).get(USER.ID)).isEqualTo(1); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/persistence/UserRolePermissionDALTest.java b/backend/src/test/java/com/zl/mjga/integration/persistence/UserRolePermissionDALTest.java new file mode 100644 index 0000000..c094b44 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/persistence/UserRolePermissionDALTest.java @@ -0,0 +1,219 @@ +package com.zl.mjga.integration.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.jooq.generated.mjga.tables.Permission.PERMISSION; +import static org.jooq.generated.mjga.tables.Role.ROLE; +import static org.jooq.generated.mjga.tables.User.USER; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.urp.PermissionQueryDto; +import com.zl.mjga.dto.urp.RoleQueryDto; +import com.zl.mjga.dto.urp.UserQueryDto; +import com.zl.mjga.model.urp.BindState; +import com.zl.mjga.repository.*; +import java.util.List; +import java.util.stream.Collectors; +import org.jooq.Record; +import org.jooq.Result; +import org.jooq.generated.mjga.tables.pojos.Permission; +import org.jooq.generated.mjga.tables.pojos.Role; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +public class UserRolePermissionDALTest extends AbstractDataAccessLayerTest { + + @Autowired private UserRoleMapRepository userRoleMapRepository; + + @Autowired private RolePermissionMapRepository rolePermissionMapRepository; + + @Autowired private UserRepository userRepository; + + @Autowired private RoleRepository roleRepository; + + @Autowired private PermissionRepository permissionRepository; + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.role (id, code, name) VALUES (1, 'testRoleA', 'testRoleA')", + "INSERT INTO mjga.user_role_map (user_id, role_id) VALUES (1, 1)" + }) + void userRoleMap_deleteByUserId() { + userRoleMapRepository.deleteByUserId(1L); + assertThat(userRoleMapRepository.fetchByUserId(1L).isEmpty()).isTrue(); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.role (id, code, name) VALUES (1, 'testRoleA', 'testRoleA')", + "INSERT INTO mjga.permission (id, code, name) VALUES (1, 'testPermissionA'," + + " 'testPermissionA')", + "INSERT INTO mjga.role_permission_map (role_id, permission_id) VALUES (1, 1)", + }) + void rolePermissionMap_deleteByRoleId() { + rolePermissionMapRepository.deleteByRoleId(1L); + assertThat(rolePermissionMapRepository.fetchByRoleId(1L).isEmpty()).isTrue(); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.user (id, username,password) VALUES (2, 'testUserB','NTjRCeUq2EqCy')", + "INSERT INTO mjga.role (id, code, name) VALUES (1, 'testRoleA', 'testRoleA')", + "INSERT INTO mjga.role (id, code, name) VALUES (2, 'testRoleB', 'testRoleB')", + "INSERT INTO mjga.permission (id, code, name) VALUES (1, 'testPermissionA'," + + " 'testPermissionA')", + "INSERT INTO mjga.permission (id, code, name) VALUES (2, 'testPermissionB'," + + " 'testPermissionB')", + "INSERT INTO mjga.user_role_map (user_id, role_id) VALUES (1, 1)", + "INSERT INTO mjga.role_permission_map (role_id, permission_id) VALUES (1, 1)", + "INSERT INTO mjga.role_permission_map (role_id, permission_id) VALUES (1, 2)", + "INSERT INTO mjga.user_role_map (user_id, role_id) VALUES (2, 2)", + "INSERT INTO mjga.role_permission_map (role_id, permission_id) VALUES (2, 2)", + }) + void user_fetchUniqueUserWithRolePermissionBy() { + Result records = userRepository.fetchUniqueUserWithRolePermissionBy(1L); + assertThat(records.size()).isEqualTo(2); + assertThat(records.get(0).get(USER.USERNAME)).isEqualTo("testUserA"); + assertThat(records.get(1).get(USER.USERNAME)).isEqualTo("testUserA"); + assertThat(records.get(0).get(ROLE.NAME)).isEqualTo("testRoleA"); + assertThat(records.get(1).get(ROLE.NAME)).isEqualTo("testRoleA"); + + List names = + records.stream().map(record -> record.get(PERMISSION.NAME)).collect(Collectors.toList()); + assertThat(names).containsExactlyInAnyOrder("testPermissionA", "testPermissionB"); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.user (id, username, password) VALUES (1, 'testA','5EUX1AIlV09n2o')", + "INSERT INTO mjga.user (id, username,password) VALUES (2, 'testB','NTjRCeUq2EqCy')", + }) + void user_pageFetchBy() { + UserQueryDto rbacQueryDto = new UserQueryDto("test"); + Result records = userRepository.pageFetchBy(PageRequestDto.of(1, 10), rbacQueryDto); + assertThat(records.size()).isEqualTo(2); + + assertThat(records.get(0).get(USER.ID)).isEqualTo(1); + assertThat(records.get(1).get(USER.ID)).isEqualTo(2); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.role (id, code, name) VALUES (1, 'testRoleA', 'testRoleA')", + "INSERT INTO mjga.role (id, code, name) VALUES (2, 'testRoleB', 'testRoleB')", + }) + void role_selectByRoleIdIn() { + List roles = roleRepository.selectByRoleIdIn(List.of(1L, 2L)); + assertThat(roles.size()).isEqualTo(2); + assertThat(roles.get(0).getId()).isEqualTo(1L); + assertThat(roles.get(1).getId()).isEqualTo(2L); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.role (id, code, name) VALUES (1, 'testRoleA', 'testRoleA')", + "INSERT INTO mjga.role (id, code, name) VALUES (2, 'testRoleB', 'testRoleB')", + }) + void role_selectByRoleCodeIn() { + List roles = roleRepository.selectByRoleCodeIn(List.of("testRoleA", "testRoleB")); + assertThat(roles.size()).isEqualTo(2); + assertThat(roles.get(0).getId()).isEqualTo(1L); + assertThat(roles.get(1).getId()).isEqualTo(2L); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.role (id, code, name) VALUES (1, 'testRoleA', 'testRoleA')", + "INSERT INTO mjga.role (id, code, name) VALUES (2, 'testRoleB', 'testRoleB')", + }) + void role_pageFetchBy() { + RoleQueryDto roleQueryDto = new RoleQueryDto(); + roleQueryDto.setRoleName("testRole"); + roleQueryDto.setBindState(BindState.ALL); + Result records = roleRepository.pageFetchBy(PageRequestDto.of(1, 10), roleQueryDto); + assertThat(records.get(0).getValue("total_role")).isEqualTo(2); + assertThat(records.get(0).getValue(ROLE.NAME)).isEqualTo("testRoleA"); + assertThat(records.get(1).getValue(ROLE.NAME)).isEqualTo("testRoleB"); + + roleQueryDto = new RoleQueryDto(); + roleQueryDto.setRoleCode("testRoleA"); + roleQueryDto.setBindState(BindState.ALL); + records = roleRepository.pageFetchBy(PageRequestDto.of(1, 10), roleQueryDto); + assertThat(records.get(0).getValue("total_role")).isEqualTo(1); + assertThat(records.get(0).getValue(ROLE.NAME)).isEqualTo("testRoleA"); + + roleQueryDto = new RoleQueryDto(); + roleQueryDto.setRoleName("test"); + roleQueryDto.setRoleCode("testRoleA"); + roleQueryDto.setBindState(BindState.ALL); + records = roleRepository.pageFetchBy(PageRequestDto.of(1, 10), roleQueryDto); + assertThat(records.get(0).getValue("total_role")).isEqualTo(1); + assertThat(records.get(0).getValue(ROLE.NAME)).isEqualTo("testRoleA"); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.role (id, code, name) VALUES (1, 'testRoleA', 'testRoleA')", + "INSERT INTO mjga.role (id, code, name) VALUES (2, 'testRoleB', 'testRoleB')", + "INSERT INTO mjga.permission (id, code, name) VALUES (1, 'testPermissionA'," + + " 'testPermissionA')", + "INSERT INTO mjga.permission (id, code, name) VALUES (2, 'testPermissionB'," + + " 'testPermissionB')", + "INSERT INTO mjga.role_permission_map (role_id, permission_id) VALUES (1, 1)", + "INSERT INTO mjga.role_permission_map (role_id, permission_id) VALUES (1, 2)", + "INSERT INTO mjga.role_permission_map (role_id, permission_id) VALUES (2, 2)", + }) + void role_fetchUniqueRoleWithPermission() { + Result records = roleRepository.fetchUniqueRoleWithPermission(1L); + assertThat(records.size()).isEqualTo(2L); + assertThat(records.get(0).getValue(ROLE.NAME)).isEqualTo("testRoleA"); + assertThat(records.get(1).getValue(ROLE.NAME)).isEqualTo("testRoleA"); + assertThat(records.get(0).getValue(PERMISSION.NAME)).isEqualTo("testPermissionA"); + assertThat(records.get(1).getValue(PERMISSION.NAME)).isEqualTo("testPermissionB"); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.permission (id, code, name) VALUES (1, 'testPermissionA'," + + " 'testPermissionA')", + "INSERT INTO mjga.permission (id, code, name) VALUES (2, 'testPermissionB'," + + " 'testPermissionB')", + }) + void permission_selectByPermissionIdIn() { + List permissions = permissionRepository.selectByPermissionIdIn(List.of(1L)); + assertThat(permissions.size()).isEqualTo(1); + assertThat(permissions.get(0).getId()).isEqualTo(1L); + } + + @Test + @Sql( + statements = { + "INSERT INTO mjga.permission (id, code, name) VALUES (1, 'testPermissionA'," + + " 'testPermissionA')", + "INSERT INTO mjga.permission (id, code, name) VALUES (2, 'testPermissionB'," + + " 'testPermissionB')", + }) + void permission_pageFetchBy() { + PermissionQueryDto permissionQueryDto = new PermissionQueryDto(); + permissionQueryDto.setPermissionCode("testPermissionA"); + permissionQueryDto.setPermissionName("test"); + permissionQueryDto.setPermissionIdList(List.of(1L)); + + Result records = + permissionRepository.pageFetchBy(PageRequestDto.of(1, 10), permissionQueryDto); + assertThat(records.get(0).getValue("total_permission")).isEqualTo(1); + assertThat(records.get(0).getValue(PERMISSION.NAME)).isEqualTo("testPermissionA"); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/quartz/AbstractQuartzTest.java b/backend/src/test/java/com/zl/mjga/integration/quartz/AbstractQuartzTest.java new file mode 100644 index 0000000..7e8f0ed --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/quartz/AbstractQuartzTest.java @@ -0,0 +1,32 @@ +package com.zl.mjga.integration.quartz; + +import com.zl.mjga.config.QuartzConfig; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jooq.JooqTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +@SpringJUnitConfig(classes = QuartzConfig.class) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Testcontainers +@JooqTest +public class AbstractQuartzTest { + public static PostgreSQLContainer postgres = + new PostgreSQLContainer<>("postgres:17.2-alpine").withDatabaseName("mjga"); + + @DynamicPropertySource + static void postgresProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgres::getJdbcUrl); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + registry.add("spring.flyway.locations", () -> "classpath:db/migration/test"); + registry.add("spring.flyway.default-schema", () -> "public"); + } + + static { + postgres.start(); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/quartz/DataBackupJobTest.java b/backend/src/test/java/com/zl/mjga/integration/quartz/DataBackupJobTest.java new file mode 100644 index 0000000..c0014bc --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/quartz/DataBackupJobTest.java @@ -0,0 +1,84 @@ +package com.zl.mjga.integration.quartz; + +import static org.junit.jupiter.api.Assertions.*; + +import java.text.MessageFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Objects; +import org.junit.jupiter.api.Test; +import org.quartz.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.quartz.CronTriggerFactoryBean; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +public class DataBackupJobTest extends AbstractQuartzTest { + @Autowired + @Qualifier("dataBackupJobDetail") private JobDetail dataBackupJobDetail; + + @Autowired + @Qualifier("dataBackupTrigger") private CronTriggerFactoryBean dataBackupTrigger; + + @Autowired + @Qualifier("dataBackupSchedulerFactory") private SchedulerFactoryBean dataBackupSchedulerFactory; + + @Test + public void dataBackupJobDetail_defineShouldValid_descShouldEqual() { + assertNotNull(dataBackupJobDetail); + assertEquals("data-backup-job", dataBackupJobDetail.getKey().getName()); + assertEquals("batch-service", dataBackupJobDetail.getKey().getGroup()); + assertTrue(dataBackupJobDetail.isDurable()); + assertEquals("Gh2mxa", dataBackupJobDetail.getJobDataMap().getString("roleId")); + } + + @Test + public void dataBackupTrigger_defineShouldValid_cronShouldBeTriggeredAtDesiredTime() { + assertNotNull(dataBackupTrigger); + CronTrigger cronTrigger = dataBackupTrigger.getObject(); + assertNotNull(cronTrigger); + assertEquals("0 0/5 * * * ?", cronTrigger.getCronExpression()); + try { + CronExpression cron = new CronExpression(cronTrigger.getCronExpression()); + Date now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2024-10-30 11:25:00"); + Date nextValidTimeAfter = cron.getNextValidTimeAfter(now); + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(now); + calendar.add(Calendar.MINUTE, 5); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + Date expectedNextTriggerTime = calendar.getTime(); + assertEquals(expectedNextTriggerTime, nextValidTimeAfter); + } catch (ParseException e) { + fail(MessageFormat.format("Invalid cron expression {0}", e)); + } + } + + @Test + public void dataBackupSchedulerFactory_defineShouldValid_descShouldEqual() + throws SchedulerException { + assertNotNull(dataBackupSchedulerFactory); + JobKey jobKey = + JobKey.jobKey( + dataBackupJobDetail.getKey().getName(), dataBackupJobDetail.getKey().getGroup()); + assertEquals( + dataBackupSchedulerFactory.getScheduler().getJobDetail(jobKey), dataBackupJobDetail); + TriggerKey triggerKey = Objects.requireNonNull(dataBackupTrigger.getObject()).getKey(); + assertEquals( + dataBackupSchedulerFactory.getScheduler().getTrigger(triggerKey), + dataBackupTrigger.getObject()); + assertEquals( + "data-backup-scheduler", dataBackupSchedulerFactory.getScheduler().getSchedulerName()); + } + + @Test + public void dataBackupScheduler_startShouldSuccess() throws SchedulerException { + Scheduler scheduler = dataBackupSchedulerFactory.getScheduler(); + assertNotNull(scheduler); + scheduler.start(); + assertTrue(scheduler.isStarted()); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/quartz/SendEmailJobTest.java b/backend/src/test/java/com/zl/mjga/integration/quartz/SendEmailJobTest.java new file mode 100644 index 0000000..13d2f03 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/quartz/SendEmailJobTest.java @@ -0,0 +1,72 @@ +package com.zl.mjga.integration.quartz; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.quartz.DateBuilder.futureDate; +import static org.quartz.JobBuilder.newJob; +import static org.quartz.TriggerBuilder.newTrigger; + +import com.zl.mjga.job.EmailJob; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.quartz.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +@Disabled +public class SendEmailJobTest extends AbstractQuartzTest { + private Boolean executed; + + @Autowired + @Qualifier("emailJobSchedulerFactory") private SchedulerFactoryBean emailJobSchedulerFactory; + + @Test + public void emailJobScheduler_givenDynamicJobAndTrigger_shouldRunJobAtDesiredTime() + throws Exception { + Scheduler emailJobScheduler = emailJobSchedulerFactory.getScheduler(); + assertTrue(emailJobScheduler.isStarted()); + JobDataMap jobDataMap = new JobDataMap(); + jobDataMap.put("userEmail", "Gh273@gmail.com"); + + JobDetail jobDetail = + newJob(EmailJob.class) + .withIdentity("email-job", "customer-service") + .usingJobData(jobDataMap) + .build(); + + Trigger trigger = + newTrigger() + .withIdentity("email-trigger", "customer-service") + .startAt(futureDate(1, DateBuilder.IntervalUnit.SECOND)) + .build(); + + emailJobScheduler + .getListenerManager() + .addJobListener( + new JobListener() { + @Override + public String getName() { + return "TestJobListener"; + } + + @Override + public void jobToBeExecuted(JobExecutionContext context) {} + + @Override + public void jobExecutionVetoed(JobExecutionContext context) {} + + @Override + public void jobWasExecuted( + JobExecutionContext context, JobExecutionException jobException) { + executed = Boolean.TRUE; + } + }); + emailJobScheduler.scheduleJob(jobDetail, trigger); + assertTrue(emailJobScheduler.isStarted()); + await() + .atMost(3, java.util.concurrent.TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(executed).isTrue()); + } +} diff --git a/backend/src/test/java/com/zl/mjga/integration/quartz/TaskManagementTest.java b/backend/src/test/java/com/zl/mjga/integration/quartz/TaskManagementTest.java new file mode 100644 index 0000000..c1e892c --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/integration/quartz/TaskManagementTest.java @@ -0,0 +1,92 @@ +package com.zl.mjga.integration.quartz; + +import static org.junit.jupiter.api.Assertions.*; +import static org.quartz.DateBuilder.futureDate; +import static org.quartz.JobBuilder.newJob; +import static org.quartz.TriggerBuilder.newTrigger; + +import com.zl.mjga.job.EmailJob; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.quartz.*; +import org.quartz.impl.matchers.GroupMatcher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.scheduling.quartz.CronTriggerFactoryBean; +import org.springframework.scheduling.quartz.JobDetailFactoryBean; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +public class TaskManagementTest extends AbstractQuartzTest { + + @Autowired + @Qualifier("emailJobSchedulerFactory") private SchedulerFactoryBean emailJobSchedulerFactory; + + @Autowired + @Qualifier("dataBackupSchedulerFactory") private SchedulerFactoryBean dataBackupSchedulerFactory; + + @Autowired + @Qualifier("dataBackupTrigger") private CronTriggerFactoryBean dataBackupTrigger; + + @Autowired + @Qualifier("dataBackupJobDetail") private JobDetailFactoryBean dataBackupJobDetail; + + @Test + void crudTask_interactWithScheduler_shouldManipulateAllTask() throws SchedulerException { + Scheduler dataBackupScheduler = dataBackupSchedulerFactory.getScheduler(); + JobDataMap jobDataMap = new JobDataMap(); + jobDataMap.put("userEmail", "Gh273@gmail.com"); + + // trigger job + JobDetail jobDetail = + newJob(EmailJob.class) + .withIdentity("email-job", "customer-service") + .usingJobData(jobDataMap) + .build(); + Trigger dayLaterTrigger = + newTrigger() + .withIdentity("email-trigger", "customer-service") + .startAt(futureDate(1, DateBuilder.IntervalUnit.DAY)) + .build(); + Scheduler emailJobScheduler = emailJobSchedulerFactory.getScheduler(); + emailJobScheduler.scheduleJob(jobDetail, dayLaterTrigger); + + // list all jobs with k(name):v(group) + Set emailJobKeys = emailJobScheduler.getJobKeys(GroupMatcher.anyJobGroup()); + Set dataBackupJobKeys = dataBackupScheduler.getJobKeys(GroupMatcher.anyJobGroup()); + + assertEquals(emailJobKeys.size(), 1); + assertEquals(dataBackupJobKeys.size(), 1); + + // get job's trigger details + JobKey firstEmailJobKey = emailJobKeys.iterator().next(); + JobDetail existJobDetail = emailJobScheduler.getJobDetail(firstEmailJobKey); + assertEquals(existJobDetail.getJobClass(), EmailJob.class); + List triggersOfJob = emailJobScheduler.getTriggersOfJob(firstEmailJobKey); + Trigger firstTrigger = triggersOfJob.get(0); + assertNotNull(firstTrigger.getNextFireTime()); + assertNotNull(firstTrigger.getStartTime()); + JobDataMap jobDataMap1 = firstTrigger.getJobDataMap(); + // pause & resume job + JobKey firstDataBackupJobKey = dataBackupJobKeys.iterator().next(); + assertDoesNotThrow( + () -> { + dataBackupScheduler.pauseJob(firstDataBackupJobKey); + dataBackupScheduler.resumeJob(firstDataBackupJobKey); + }); + + // update job + TriggerKey oldDataBackupTriggerKey = + Objects.requireNonNull(dataBackupTrigger.getObject()).getKey(); + Trigger newTrigger = + TriggerBuilder.newTrigger() + .withIdentity(oldDataBackupTriggerKey) + .withSchedule(CronScheduleBuilder.cronSchedule("0 0/6 * * * ?")) + .build(); + dataBackupScheduler.rescheduleJob(oldDataBackupTriggerKey, newTrigger); + assertEquals( + dataBackupScheduler.getTriggersOfJob(firstDataBackupJobKey).get(0).getKey(), + newTrigger.getKey()); + } +} diff --git a/backend/src/test/java/com/zl/mjga/security/AuthenticationAndAuthorityTest.java b/backend/src/test/java/com/zl/mjga/security/AuthenticationAndAuthorityTest.java new file mode 100644 index 0000000..681d177 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/security/AuthenticationAndAuthorityTest.java @@ -0,0 +1,128 @@ +package com.zl.mjga.security; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.zl.mjga.config.security.HttpFireWallConfig; +import com.zl.mjga.config.security.Jwt; +import com.zl.mjga.config.security.UserDetailsServiceImpl; +import com.zl.mjga.config.security.WebSecurityConfig; +import com.zl.mjga.controller.IdentityAccessController; +import com.zl.mjga.controller.SignController; +import com.zl.mjga.dto.sign.SignInDto; +import com.zl.mjga.model.urp.EPermission; +import com.zl.mjga.repository.RoleRepository; +import com.zl.mjga.repository.UserRepository; +import com.zl.mjga.service.IdentityAccessService; +import com.zl.mjga.service.SignService; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(value = {SignController.class, IdentityAccessController.class}) +@Import({WebSecurityConfig.class, HttpFireWallConfig.class}) +public class AuthenticationAndAuthorityTest { + + @Autowired private MockMvc mockMvc; + + @MockBean private SignService signService; + + @MockBean private Jwt jwt; + + @MockBean private UserDetailsServiceImpl userDetailsService; + + @MockBean private IdentityAccessService identityAccessService; + + @MockBean private UserRepository userRepository; + @MockBean private RoleRepository roleRepository; + + @Test + public void givenRequestOnPublicService_shouldSucceedWith200() throws Exception { + when(signService.signIn(any(SignInDto.class))).thenReturn(1L); + mockMvc + .perform( + post("/auth/sign-in") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "test_04cb017e1fe6", + "password": "test_567472858b8c" + } + """) + .with(csrf())) + .andExpect(status().isOk()); + } + + @Test + public void givenUnAuthenticateRequestOnPrivateService_shouldFailedWith401() throws Exception { + mockMvc.perform(post("/auth/sign-out").with(csrf())).andExpect(status().isUnauthorized()); + } + + @Test + public void givenUnAuthorityRequestOnPrivateService_shouldFailedWith403() throws Exception { + // Arrange + User stubUserNoPermission = + new User("test_04cb017e1fe6", "test_567472858b8c", Collections.emptyList()); + when(jwt.extract(any(HttpServletRequest.class))).thenReturn(("u9T05Tg3ULCgRn8ja2")); + when(jwt.getSubject(any(String.class))).thenReturn(("4J2HX9r5JcXg0BT")); + when(jwt.verify(any(String.class))).thenReturn(Boolean.TRUE); + when(userDetailsService.loadUserByUsername(any(String.class))).thenReturn(stubUserNoPermission); + + // Act and Assert + mockMvc + .perform( + post("/iam/permission/bind") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "roleId": 1, + "permissionIds": [101, 102] + } + """)) + .andExpect(status().isForbidden()); + } + + @Test + public void givenAuthorityRequestOnPrivateService_shouldSuccessWith200() throws Exception { + // Arrange + User stubUserNoPermission = + new User( + "test_04cb017e1fe6", + "test_567472858b8c", + List.of(new SimpleGrantedAuthority(EPermission.WRITE_USER_ROLE_PERMISSION.toString()))); + when(jwt.extract(any(HttpServletRequest.class))).thenReturn(("u9T05Tg3ULCgRn8ja2")); + when(jwt.getSubject(any(String.class))).thenReturn(("4J2HX9r5JcXg0BT")); + when(jwt.verify(any(String.class))).thenReturn(Boolean.TRUE); + when(userDetailsService.loadUserByUsername(any(String.class))).thenReturn(stubUserNoPermission); + + // Act and Assert + mockMvc + .perform( + post("/iam/permission/bind") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "roleId": 1, + "permissionIds": [101, 102] + } + """)) + .andExpect(status().isOk()); + } +} diff --git a/backend/src/test/java/com/zl/mjga/unit/JwtUnitTest.java b/backend/src/test/java/com/zl/mjga/unit/JwtUnitTest.java new file mode 100644 index 0000000..5747e9a --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/unit/JwtUnitTest.java @@ -0,0 +1,79 @@ +package com.zl.mjga.unit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.zl.mjga.config.security.Jwt; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class JwtUnitTest { + + @Spy private Jwt jwt = new Jwt("M3pIZlfyzkJ5Hi9OL", 60); + + @Mock private HttpServletRequest request; + + @Mock private HttpServletResponse response; + + @Test + void createVerifyGetSubjectJwt_givenUserIdentify_shouldReturnTrueAndGetExpectIdentify() { + String token = jwt.create("1"); + assertThat(jwt.verify(token)).isTrue(); + assertThat(jwt.getSubject(token)).isEqualTo("1"); + } + + @Test + void getSubject_whenTokenIsInvalid_shouldThrowJWTDecodeException() { + String invalidToken = "invalid.token.here"; + assertThatThrownBy(() -> jwt.getSubject(invalidToken)).isInstanceOf(JWTDecodeException.class); + } + + @Test + void getSubject_whenTokenHasDifferentSecret_shouldReturnSubject() { + Jwt otherJwt = new Jwt("different_secret", 60); + String token = otherJwt.create("user123"); + + assertThat(jwt.verify(token)).isFalse(); + assertThat(jwt.getSubject(token)).isEqualTo("user123"); + } + + @Test + void getSubject_whenTokenIsNull_shouldThrowException() { + assertThatThrownBy(() -> jwt.getSubject(null)).isInstanceOf(JWTDecodeException.class); + } + + @Test + void create_WithVariousUserIdentifiers_ShouldCorrectlySetSubject() { + String[] identifiers = {"", "user@domain.com", "12345", "!@#$%"}; + for (String id : identifiers) { + String token = jwt.create(id); + assertThat(jwt.getSubject(token)).isEqualTo(id); + } + } + + @Test + void create_withDifferentSecret_shouldFailVerification() { + Jwt otherJwt = new Jwt("different_secret", 60); + String token = otherJwt.create("user"); + + assertThat(jwt.verify(token)).isFalse(); + } + + @Test + void create_WhenExpirationMinIsZero_shouldExpireImmediately() { + Jwt zeroExpirationJwt = new Jwt("secret", 0); + String token = zeroExpirationJwt.create("test"); + DecodedJWT decoded = JWT.decode(token); + + assertThat(decoded.getExpiresAt()).isEqualTo(decoded.getIssuedAt()); + } +} diff --git a/backend/src/test/java/com/zl/mjga/unit/PageRequestDtoUnitTest.java b/backend/src/test/java/com/zl/mjga/unit/PageRequestDtoUnitTest.java new file mode 100644 index 0000000..2da7882 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/unit/PageRequestDtoUnitTest.java @@ -0,0 +1,70 @@ +package com.zl.mjga.unit; + +import static org.assertj.core.api.Assertions.*; + +import com.zl.mjga.dto.PageRequestDto; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class PageRequestDtoUnitTest { + + @Test + void setSortBy_whenSortByFieldIsExpectFormat_thenDeserializeCorrect() { + String sortBy1 = "id asc,name desc"; + String sortBy2 = "id asc"; + String sortBy3 = "id asc,"; + String sortBy4 = ","; + String sortBy5 = ""; + PageRequestDto pageRequestDto1 = new PageRequestDto(); + PageRequestDto pageRequestDto2 = new PageRequestDto(); + PageRequestDto pageRequestDto3 = new PageRequestDto(); + PageRequestDto pageRequestDto4 = new PageRequestDto(); + PageRequestDto pageRequestDto5 = new PageRequestDto(); + pageRequestDto1.setSortBy(sortBy1); + pageRequestDto2.setSortBy(sortBy2); + pageRequestDto3.setSortBy(sortBy3); + pageRequestDto4.setSortBy(sortBy4); + pageRequestDto5.setSortBy(sortBy5); + assertThat( + pageRequestDto1 + .getSortBy() + .equals( + Map.of( + "id", PageRequestDto.Direction.ASC, "name", PageRequestDto.Direction.DESC))) + .isTrue(); + assertThat(pageRequestDto2.getSortBy().equals(Map.of("id", PageRequestDto.Direction.ASC))) + .isTrue(); + assertThat(pageRequestDto3.getSortBy().equals(Map.of("id", PageRequestDto.Direction.ASC))) + .isTrue(); + assertThat(pageRequestDto4.getSortBy().equals(new HashMap<>())).isTrue(); + assertThat(pageRequestDto5.getSortBy().equals(new HashMap<>())).isTrue(); + } + + @Test + void setSortBy_whenSortByFieldInvalidFormat_thenRaiseError() { + String sortBy1 = "id bbb"; + String sortBy2 = "2%^ asc"; + String sortBy3 = "id asc,*&23 desc"; + String sortBy4 = "id,name desc"; + String sortBy5 = ",name asc"; + PageRequestDto pageRequestDto = new PageRequestDto(); + assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy1)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy2)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy3)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy4)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> pageRequestDto.setSortBy(sortBy5)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/backend/src/test/java/com/zl/mjga/unit/SignUnitTest.java b/backend/src/test/java/com/zl/mjga/unit/SignUnitTest.java new file mode 100644 index 0000000..2bca40f --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/unit/SignUnitTest.java @@ -0,0 +1,103 @@ +package com.zl.mjga.unit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import com.zl.mjga.dto.sign.SignInDto; +import com.zl.mjga.dto.sign.SignUpDto; +import com.zl.mjga.exception.BusinessException; +import com.zl.mjga.model.urp.ERole; +import com.zl.mjga.repository.UserRepository; +import com.zl.mjga.service.IdentityAccessService; +import com.zl.mjga.service.SignService; +import java.util.List; +import org.jooq.generated.mjga.tables.pojos.User; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +public class SignUnitTest { + @InjectMocks @Spy private SignService signService; + + @Mock private UserRepository userRepository; + + @Mock private PasswordEncoder passwordEncoder; + + @Mock private IdentityAccessService identityAccessService; + + @Test + void signIn_givenValidSignInfo_shouldReturnUserId() { + // arrange + User stubUser = new User(); + stubUser.setId(1L); + stubUser.setUsername("testUserName"); + stubUser.setPassword("GjFH2fzRB2y7DDrO"); + when(userRepository.fetchOneByUsername("testUserName")).thenReturn(stubUser); + when(passwordEncoder.matches("GjFH2fzRB2y7DDrO", "GjFH2fzRB2y7DDrO")).thenReturn(true); + // action + Long userId = signService.signIn(new SignInDto("testUserName", "GjFH2fzRB2y7DDrO")); + assertThat(userId).isEqualTo(1L); + } + + @Test + void signIn_givenInvalidUserName_shouldThrowUserNotFoundException() { + when(userRepository.fetchOneByUsername("notFoundUserName")).thenReturn(null); + assertThatThrownBy( + () -> signService.signIn(new SignInDto("notFoundUserName", "GjFH2fzRB2y7DDrO"))) + .isInstanceOf(BusinessException.class); + } + + @Test + void signIn_givenInvalidPassword_shouldThrowBadCredentialsException() { + // arrange + User stubUser = new User(); + stubUser.setId(1L); + stubUser.setUsername("testUserName"); + stubUser.setPassword("GjFH2fzRB2y7DDrO"); + when(userRepository.fetchOneByUsername("testUserName")).thenReturn(stubUser); + when(passwordEncoder.matches("InvalidPassword", "GjFH2fzRB2y7DDrO")).thenReturn(false); + // action + assertThatThrownBy(() -> signService.signIn(new SignInDto("testUserName", "InvalidPassword"))) + .isInstanceOf(BusinessException.class); + } + + @Test + void signUp_givenDuplicateUsername_shouldThrowDuplicateException() { + SignUpDto signUpDto = new SignUpDto(); + signUpDto.setUsername("testUserName"); + signUpDto.setPassword("B0pjKYnIK67hz4"); + User stubUser = new User(); + stubUser.setId(1L); + stubUser.setUsername("testUserName"); + stubUser.setPassword("B0pjKYnIK67hz4"); + when(identityAccessService.isUsernameDuplicate(signUpDto.getUsername())).thenReturn(true); + assertThatThrownBy(() -> signService.signUp(signUpDto)).isInstanceOf(BusinessException.class); + } + + @Test + void signUp_givenValidUsername_shouldRunSuccess() { + SignUpDto signUpDto = new SignUpDto(); + signUpDto.setUsername("newUser"); + signUpDto.setPassword("B0pjKYnIK67hz4"); + User stubUser = new User(); + stubUser.setUsername("newUser"); + stubUser.setPassword("encodedB0pjKYnIK67hz4"); + User insertUser = new User(); + insertUser.setId(1L); + insertUser.setUsername("newUser"); + insertUser.setPassword("encodedB0pjKYnIK67hz4"); + when(identityAccessService.isUsernameDuplicate(signUpDto.getUsername())).thenReturn(false); + when(userRepository.fetchOneByUsername("newUser")).thenReturn(insertUser); + when(passwordEncoder.encode("B0pjKYnIK67hz4")).thenReturn("encodedB0pjKYnIK67hz4"); + signService.signUp(signUpDto); + verify(userRepository, times(1)).insert(stubUser); + verify(identityAccessService, times(1)) + .bindRoleModuleToUser(insertUser.getId(), List.of(ERole.GENERAL)); + } +} diff --git a/backend/src/test/java/com/zl/mjga/unit/UserRolePermissionUnitTest.java b/backend/src/test/java/com/zl/mjga/unit/UserRolePermissionUnitTest.java new file mode 100644 index 0000000..bd8ffd5 --- /dev/null +++ b/backend/src/test/java/com/zl/mjga/unit/UserRolePermissionUnitTest.java @@ -0,0 +1,408 @@ +package com.zl.mjga.unit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.jooq.generated.mjga.tables.Permission.PERMISSION; +import static org.jooq.generated.mjga.tables.Role.ROLE; +import static org.jooq.generated.mjga.tables.User.USER; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; + +import com.zl.mjga.dto.PageRequestDto; +import com.zl.mjga.dto.PageResponseDto; +import com.zl.mjga.dto.urp.*; +import com.zl.mjga.repository.*; +import com.zl.mjga.service.IdentityAccessService; +import java.sql.SQLException; +import java.util.List; +import org.jooq.*; +import org.jooq.Record; +import org.jooq.generated.mjga.tables.pojos.*; +import org.jooq.generated.mjga.tables.pojos.Role; +import org.jooq.generated.mjga.tables.pojos.User; +import org.jooq.impl.DSL; +import org.jooq.tools.jdbc.MockConnection; +import org.jooq.tools.jdbc.MockDataProvider; +import org.jooq.tools.jdbc.MockResult; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.BeanUtils; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class UserRolePermissionUnitTest { + @InjectMocks @Spy private IdentityAccessService identityAccessService; + + @Mock private UserRepository userRepository; + + @Mock private RoleRepository roleRepository; + @Mock private UserRoleMapRepository userRoleMapRepository; + @Mock private PermissionRepository permissionRepository; + @Mock private RolePermissionMapRepository rolePermissionMapRepository; + @Mock private PasswordEncoder passwordEncoder; + + private static DSLContext dslContext; + + private static MockConnection connection; + + @BeforeAll + static void setUp() { + MockDataProvider provider = ctx -> new MockResult[0]; + connection = new MockConnection(provider); + dslContext = DSL.using(connection, SQLDialect.POSTGRES); + } + + @AfterAll + static void setDown() throws SQLException { + connection.close(); + } + + @Test + void pageQueryUser_selected2UserShouldReturnUserRolePermissionAndTotal() { + // arrange + Long stubUserId1 = 1L; + String stubUserName1 = "yEJVEJBC2j9PGi"; + String stubUserPassword1 = "c21W03p1201jCz"; + + Long stubUserId2 = 2L; + String stubUserName2 = "1jpziB82YUs3Jbh"; + String stubUserPassword2 = "c21W03p1201jCz"; + + Long stubRoleId = 1L; + String stubRoleName = "54X3UYRzx0wiy9"; + String stubRoleCode = "mzxN6WQA3AErI"; + + Long stubPermissionId = 1L; + String stubPermissionName = "BNOz058K9EWE"; + String stubPermissionCode = "BNOz058K9EWE"; + + Result mockResult = + dslContext.newResult( + List.of( + USER.ID, + USER.USERNAME, + USER.PASSWORD, + USER.ENABLE, + USER.CREATE_TIME, + DSL.field("total_user", Integer.class))); + mockResult.add( + dslContext + .newRecord( + USER.ID, + USER.USERNAME, + USER.PASSWORD, + USER.ENABLE, + USER.CREATE_TIME, + DSL.field("total_user", Integer.class)) + .values(stubUserId1, stubUserName2, stubUserPassword2, true, null, 2)); + mockResult.add( + dslContext + .newRecord( + USER.ID, + USER.USERNAME, + USER.PASSWORD, + USER.ENABLE, + USER.CREATE_TIME, + DSL.field("total_user", Integer.class)) + .values(stubUserId2, stubUserName2, stubUserPassword2, true, null, 2)); + UserRolePermissionDto mockUserRolePermissionDto1 = new UserRolePermissionDto(); + RoleDto mockRoleDto = new RoleDto(); + mockRoleDto.setId(stubRoleId); + mockRoleDto.setCode(stubRoleCode); + mockRoleDto.setName(stubRoleName); + PermissionRespDto permissionRespDto = new PermissionRespDto(); + permissionRespDto.setId(stubPermissionId); + permissionRespDto.setCode(stubPermissionCode); + permissionRespDto.setName(stubPermissionName); + mockRoleDto.getPermissions().add(permissionRespDto); + mockUserRolePermissionDto1.setId(stubUserId1); + mockUserRolePermissionDto1.setUsername(stubUserName1); + mockUserRolePermissionDto1.setPassword(stubUserPassword1); + mockUserRolePermissionDto1.getRoles().add(mockRoleDto); + + UserRolePermissionDto mockUserRolePermissionDto2 = new UserRolePermissionDto(); + mockUserRolePermissionDto2.setId(stubUserId2); + mockUserRolePermissionDto2.setUsername(stubUserName2); + mockUserRolePermissionDto2.setPassword(stubUserPassword2); + + doReturn(mockUserRolePermissionDto1) + .when(identityAccessService) + .queryUniqueUserWithRolePermission(stubUserId1); + doReturn(mockUserRolePermissionDto2) + .when(identityAccessService) + .queryUniqueUserWithRolePermission(stubUserId2); + when(userRepository.pageFetchBy(any(PageRequestDto.class), any(UserQueryDto.class))) + .thenReturn(mockResult); + + // action + PageResponseDto> result = + identityAccessService.pageQueryUser( + PageRequestDto.of(1, 10), new UserQueryDto(stubUserName2)); + + // assert + List userRolePermissionDtoList = result.getData(); + assertThat(result.getTotal()).isEqualTo(2L); + assertThat(userRolePermissionDtoList.size()).isEqualTo(2L); + assertThat(userRolePermissionDtoList.get(0).getRoles().size()).isEqualTo(1L); + assertThat(userRolePermissionDtoList.get(1).getRoles().size()).isEqualTo(0L); + assertThat(userRolePermissionDtoList.get(1).getUsername()).isEqualTo(stubUserName2); + assertThat(userRolePermissionDtoList.get(0).getRoles().get(0).getName()) + .isEqualTo(stubRoleName); + assertThat(userRolePermissionDtoList.get(0).getRoles().get(0).getPermissions().get(0).getCode()) + .isEqualTo(stubPermissionCode); + } + + @Test + void queryUser_selected0Row_shouldReturnEmptyListWithPage() { + Result mockResult = + dslContext.newResult( + List.of( + USER.ID, + USER.USERNAME, + USER.PASSWORD, + USER.ENABLE, + USER.CREATE_TIME, + DSL.field("total_user", Integer.class))); + when(userRepository.pageFetchBy(any(PageRequestDto.class), any(UserQueryDto.class))) + .thenReturn(mockResult); + PageResponseDto> result = + identityAccessService.pageQueryUser( + PageRequestDto.of(1, 10), new UserQueryDto("agydCO1Yi99a")); + assertThat(result.getTotal()).isEqualTo(0); + assertThat(result.getData()).isNull(); + } + + @Test + void + queryUniqueUserWithRolePermission_whenUserHasBeenFound_shouldReturnUniqueUserRolePermissionDto() { + Long stubUserId = 1L; + String stubUserName = "yEJVEJBC2j9PGi"; + String stubUserPassword = "c21W03p1201jCz"; + Long stubRoleId = 1L; + String stubRoleName = "G5N6Xkjg0i9UC4Vltv"; + String stubRoleCode = "G5N6Xkjg0i9UC4Vltv"; + + Long stubPermissionId = 1L; + String stubPermissionName = "BNOz058K9EWE"; + String stubPermissionCode = "BNOz058K9EWE"; + + Long stubPermissionId2 = 2L; + String stubPermissionName2 = "u6igc4BctOm1ON6X"; + String stubPermissionCode2 = "u6igc4BctOm1ON6X"; + + UserRolePermissionDto mockResult = new UserRolePermissionDto(); + mockResult.setUsername(stubUserName); + mockResult.setPassword(stubUserPassword); + mockResult.setId(stubRoleId); + mockResult.setRoles( + List.of( + new RoleDto( + stubRoleId, + stubRoleName, + stubRoleCode, + true, + List.of( + new PermissionRespDto( + stubPermissionId, stubPermissionName, stubPermissionCode, false), + new PermissionRespDto( + stubPermissionId2, stubPermissionName2, stubPermissionCode2, false))))); + + when(userRepository.fetchUniqueUserDtoWithNestedRolePermissionBy(stubUserId)) + .thenReturn(mockResult); + UserRolePermissionDto userRolePermissionDto = + identityAccessService.queryUniqueUserWithRolePermission(stubUserId); + assertThat(userRolePermissionDto).isNotNull(); + assertThat(userRolePermissionDto.getRoles().size()).isEqualTo(1L); + assertThat(userRolePermissionDto.getRoles().get(0).getPermissions().size()).isEqualTo(2L); + assertThat(userRolePermissionDto.getUsername()).isEqualTo(stubUserName); + assertThat(userRolePermissionDto.getRoles().get(0).getPermissions().get(0).getName()) + .isEqualTo(stubPermissionName); + assertThat(userRolePermissionDto.getRoles().get(0).getPermissions().get(0).getCode()) + .isEqualTo(stubPermissionCode); + } + + @Test + void queryUniqueUserWithRolePermission_whenUserNotFound_shouldReturnEmpty() { + UserRolePermissionDto mockResult = null; + when(userRepository.fetchUniqueUserDtoWithNestedRolePermissionBy(anyLong())) + .thenReturn(mockResult); + UserRolePermissionDto userRolePermissionDto = + identityAccessService.queryUniqueUserWithRolePermission(1L); + assertThat(userRolePermissionDto).isNull(); + } + + @Test + void pageQueryRole_whenRoleNotFound_shouldReturnEmpty() { + Result mockRoleResult = + dslContext.newResult( + List.of(ROLE.ID, ROLE.NAME, ROLE.CODE, DSL.field("total_role", Integer.class))); + when(roleRepository.pageFetchBy(any(PageRequestDto.class), any(RoleQueryDto.class))) + .thenReturn(mockRoleResult); + RoleQueryDto roleQueryDto = new RoleQueryDto(); + PageResponseDto> pageResult = + identityAccessService.pageQueryRole(PageRequestDto.of(1, 5), roleQueryDto); + assertThat(pageResult.getTotal()).isEqualTo(0L); + + roleQueryDto.setUserId(1L); + PageResponseDto> pageResult2 = + identityAccessService.pageQueryRole(PageRequestDto.of(1, 5), roleQueryDto); + assertThat(pageResult2.getTotal()).isEqualTo(0L); + } + + @Test + void pageQueryPermission_givenRoleId_shouldReturnPermissionDto() { + RolePermissionMap stubRolePermissionMap = new RolePermissionMap(); + stubRolePermissionMap.setRoleId(1L); + stubRolePermissionMap.setPermissionId(1L); + RolePermissionMap stubRolePermissionMap2 = new RolePermissionMap(); + stubRolePermissionMap2.setRoleId(1L); + stubRolePermissionMap2.setPermissionId(2L); + + Result mockRoleResult = + dslContext.newResult( + List.of( + PERMISSION.ID, + PERMISSION.NAME, + PERMISSION.CODE, + DSL.field("total_permission", Integer.class))); + mockRoleResult.addAll( + List.of( + dslContext + .newRecord( + PERMISSION.ID, + PERMISSION.NAME, + PERMISSION.CODE, + DSL.field("total_permission", Integer.class)) + .values(1L, "vP0dKiHJpMsi", "vP0dKiHJpMsi", 2), + dslContext + .newRecord( + PERMISSION.ID, + PERMISSION.NAME, + PERMISSION.CODE, + DSL.field("total_permission", Integer.class)) + .values(2L, "NHQED41jQQ4C1IgG", "NHQED41jQQ4C1IgG", 2))); + when(permissionRepository.pageFetchBy(any(PageRequestDto.class), any(PermissionQueryDto.class))) + .thenReturn(mockRoleResult); + PermissionQueryDto permissionQueryDto = new PermissionQueryDto(); + permissionQueryDto.setRoleId(1L); + PageResponseDto> pageResult = + identityAccessService.pageQueryPermission(PageRequestDto.of(1, 5), permissionQueryDto); + assertThat(pageResult.getTotal()).isEqualTo(2L); + List permissionResult = pageResult.getData(); + assertThat(permissionResult.get(0).getId()).isEqualTo(1L); + assertThat(permissionResult.get(1).getId()).isEqualTo(2L); + } + + @Test + void pageQueryPermission_permissionNotFound_shouldReturnEmpty() { + Result mockRoleResult = + dslContext.newResult( + List.of( + PERMISSION.ID, + PERMISSION.NAME, + PERMISSION.CODE, + DSL.field("total_permission", Integer.class))); + when(permissionRepository.pageFetchBy(any(PageRequestDto.class), any(PermissionQueryDto.class))) + .thenReturn(mockRoleResult); + PermissionQueryDto permissionQueryDto = new PermissionQueryDto(); + PageResponseDto> pageResult = + identityAccessService.pageQueryPermission(PageRequestDto.of(1, 5), permissionQueryDto); + + assertThat(pageResult.getTotal()).isEqualTo(0L); + permissionQueryDto.setRoleId(1L); + PageResponseDto> pageResult2 = + identityAccessService.pageQueryPermission(PageRequestDto.of(1, 5), permissionQueryDto); + assertThat(pageResult2.getTotal()).isEqualTo(0); + } + + @Test + void upsertUser_whenGivenUserDtoWithOutId_shouldCreatUser() { + UserUpsertDto userUpsertDto = new UserUpsertDto(); + userUpsertDto.setUsername("username"); + userUpsertDto.setPassword("password"); + userUpsertDto.setEnable(true); + User mockUser = new User(); + BeanUtils.copyProperties(userUpsertDto, mockUser); + when(passwordEncoder.encode(anyString())).thenReturn("password"); + identityAccessService.upsertUser(userUpsertDto); + verify(userRepository, times(1)).mergeWithoutNullFieldBy(mockUser); + } + + @Test + void upsertUser_whenGivenUserDtoWithId_shouldUpdateUser() { + UserUpsertDto userUpsertDto = new UserUpsertDto(); + userUpsertDto.setId(1L); + userUpsertDto.setUsername("username"); + userUpsertDto.setPassword("password"); + userUpsertDto.setEnable(true); + User mockUser = new User(); + BeanUtils.copyProperties(userUpsertDto, mockUser); + when(passwordEncoder.encode(anyString())).thenReturn("password"); + identityAccessService.upsertUser(userUpsertDto); + verify(userRepository, times(1)).mergeWithoutNullFieldBy(mockUser); + } + + @Test + void upsertRole_whenGivenRoleDtoWithOutId_shouldCreateRole() { + RoleUpsertDto roleUpsertDto = new RoleUpsertDto(); + roleUpsertDto.setCode("ROLE_ADMIN"); + roleUpsertDto.setName("Admin Role"); + + Role mockRole = new Role(); + BeanUtils.copyProperties(roleUpsertDto, mockRole); + + identityAccessService.upsertRole(roleUpsertDto); + + verify(roleRepository, times(1)).merge(mockRole); + } + + @Test + void upsertRole_whenGivenRoleDtoWithId_shouldUpdateRole() { + RoleUpsertDto roleUpsertDto = new RoleUpsertDto(); + roleUpsertDto.setId(1L); + roleUpsertDto.setCode("ROLE_ADMIN"); + roleUpsertDto.setName("Admin Role"); + + Role mockRole = new Role(); + BeanUtils.copyProperties(roleUpsertDto, mockRole); + + identityAccessService.upsertRole(roleUpsertDto); + + verify(roleRepository, times(1)).merge(mockRole); + } + + @Test + void upsertPermission_whenGivenPermissionDtoWithOutId_shouldCreatePermission() { + PermissionUpsertDto permissionUpsertDto = new PermissionUpsertDto(); + permissionUpsertDto.setCode("PERM_READ"); + permissionUpsertDto.setName("Read Permission"); + + Permission mockPermission = new Permission(); + BeanUtils.copyProperties(permissionUpsertDto, mockPermission); + + identityAccessService.upsertPermission(permissionUpsertDto); + + verify(permissionRepository, times(1)).merge(mockPermission); + } + + @Test + void upsertPermission_whenGivenPermissionDtoWithId_shouldUpdatePermission() { + PermissionUpsertDto permissionUpsertDto = new PermissionUpsertDto(); + permissionUpsertDto.setId(1L); + permissionUpsertDto.setCode("PERM_READ"); + permissionUpsertDto.setName("Read Permission"); + + Permission mockPermission = new Permission(); + BeanUtils.copyProperties(permissionUpsertDto, mockPermission); + + identityAccessService.upsertPermission(permissionUpsertDto); + + verify(permissionRepository, times(1)).merge(mockPermission); + } +} diff --git a/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql b/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql new file mode 100644 index 0000000..2cef2f3 --- /dev/null +++ b/backend/src/test/resources/db/migration/test/V1_0_0__init_table.sql @@ -0,0 +1,67 @@ +CREATE SCHEMA IF NOT EXISTS mjga; + +CREATE TABLE mjga.user ( + id BIGSERIAL PRIMARY KEY, + username VARCHAR NOT NULL UNIQUE, + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + password VARCHAR NOT NULL, + enable BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE mjga.permission ( + id BIGSERIAL PRIMARY KEY, + code VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL UNIQUE +); + +CREATE TABLE mjga.role ( + id BIGSERIAL PRIMARY KEY, + code VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL UNIQUE +); + +CREATE TABLE mjga.role_permission_map ( + role_id BIGINT NOT NULL, + permission_id BIGINT NOT NULL, + PRIMARY KEY (role_id, permission_id), + FOREIGN KEY (role_id) REFERENCES mjga.role(id) ON DELETE RESTRICT, + FOREIGN KEY (permission_id) REFERENCES mjga.permission(id) ON DELETE RESTRICT +); + +CREATE TABLE mjga.user_role_map ( + user_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE RESTRICT, + FOREIGN KEY (role_id) REFERENCES mjga.role(id) ON DELETE RESTRICT +); + +CREATE TABLE mjga.department ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + parent_id BIGINT, + FOREIGN KEY (parent_id) + REFERENCES mjga.department(id) + ON DELETE RESTRICT +); + +CREATE TABLE mjga.user_department_map ( + user_id BIGINT NOT NULL, + department_id BIGINT NOT NULL, + PRIMARY KEY (user_id, department_id), + FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON UPDATE NO ACTION ON DELETE RESTRICT, + FOREIGN KEY (department_id) REFERENCES mjga.department(id) ON UPDATE NO ACTION ON DELETE RESTRICT +); + +CREATE TABLE mjga.position ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE +); + +CREATE TABLE mjga.user_position_map ( + user_id BIGINT NOT NULL, + position_id BIGINT NOT NULL, + PRIMARY KEY (user_id, position_id), + FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON UPDATE NO ACTION ON DELETE RESTRICT, + FOREIGN KEY (position_id) REFERENCES mjga.position(id) ON UPDATE NO ACTION ON DELETE RESTRICT +); \ No newline at end of file diff --git a/backend/src/test/resources/db/migration/test/V1_0_2__init_quartz.sql b/backend/src/test/resources/db/migration/test/V1_0_2__init_quartz.sql new file mode 100644 index 0000000..bc08e12 --- /dev/null +++ b/backend/src/test/resources/db/migration/test/V1_0_2__init_quartz.sql @@ -0,0 +1,194 @@ +CREATE SCHEMA IF NOT EXISTS public; + +SET search_path TO public; + +CREATE TABLE QRTZ_JOB_DETAILS +( + SCHED_NAME VARCHAR(120) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + JOB_CLASS_NAME VARCHAR(250) NOT NULL, + IS_DURABLE BOOL NOT NULL, + IS_NONCONCURRENT BOOL NOT NULL, + IS_UPDATE_DATA BOOL NOT NULL, + REQUESTS_RECOVERY BOOL NOT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) +); + +CREATE TABLE QRTZ_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + NEXT_FIRE_TIME BIGINT NULL, + PREV_FIRE_TIME BIGINT NULL, + PRIORITY INTEGER NULL, + TRIGGER_STATE VARCHAR(16) NOT NULL, + TRIGGER_TYPE VARCHAR(8) NOT NULL, + START_TIME BIGINT NOT NULL, + END_TIME BIGINT NULL, + CALENDAR_NAME VARCHAR(200) NULL, + MISFIRE_INSTR SMALLINT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) + REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP) +); + +CREATE TABLE QRTZ_SIMPLE_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + REPEAT_COUNT BIGINT NOT NULL, + REPEAT_INTERVAL BIGINT NOT NULL, + TIMES_TRIGGERED BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_CRON_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + CRON_EXPRESSION VARCHAR(120) NOT NULL, + TIME_ZONE_ID VARCHAR(80), + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_SIMPROP_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + STR_PROP_1 VARCHAR(512) NULL, + STR_PROP_2 VARCHAR(512) NULL, + STR_PROP_3 VARCHAR(512) NULL, + INT_PROP_1 INT NULL, + INT_PROP_2 INT NULL, + LONG_PROP_1 BIGINT NULL, + LONG_PROP_2 BIGINT NULL, + DEC_PROP_1 NUMERIC(13, 4) NULL, + DEC_PROP_2 NUMERIC(13, 4) NULL, + BOOL_PROP_1 BOOL NULL, + BOOL_PROP_2 BOOL NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_BLOB_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + BLOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_CALENDARS +( + SCHED_NAME VARCHAR(120) NOT NULL, + CALENDAR_NAME VARCHAR(200) NOT NULL, + CALENDAR BYTEA NOT NULL, + PRIMARY KEY (SCHED_NAME, CALENDAR_NAME) +); + + +CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_FIRED_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + ENTRY_ID VARCHAR(95) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + INSTANCE_NAME VARCHAR(200) NOT NULL, + FIRED_TIME BIGINT NOT NULL, + SCHED_TIME BIGINT NOT NULL, + PRIORITY INTEGER NOT NULL, + STATE VARCHAR(16) NOT NULL, + JOB_NAME VARCHAR(200) NULL, + JOB_GROUP VARCHAR(200) NULL, + IS_NONCONCURRENT BOOL NULL, + REQUESTS_RECOVERY BOOL NULL, + PRIMARY KEY (SCHED_NAME, ENTRY_ID) +); + +CREATE TABLE QRTZ_SCHEDULER_STATE +( + SCHED_NAME VARCHAR(120) NOT NULL, + INSTANCE_NAME VARCHAR(200) NOT NULL, + LAST_CHECKIN_TIME BIGINT NOT NULL, + CHECKIN_INTERVAL BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, INSTANCE_NAME) +); + +CREATE TABLE QRTZ_LOCKS +( + SCHED_NAME VARCHAR(120) NOT NULL, + LOCK_NAME VARCHAR(40) NOT NULL, + PRIMARY KEY (SCHED_NAME, LOCK_NAME) +); + +CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY + ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY); +CREATE INDEX IDX_QRTZ_J_GRP + ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP); + +CREATE INDEX IDX_QRTZ_T_J + ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_T_JG + ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_T_C + ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME); +CREATE INDEX IDX_QRTZ_T_G + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); +CREATE INDEX IDX_QRTZ_T_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_N_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_N_G_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME + ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_ST + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE); + +CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME); +CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY); +CREATE INDEX IDX_QRTZ_FT_J_G + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_FT_JG + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_FT_T_G + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP); +CREATE INDEX IDX_QRTZ_FT_TG + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); + + +COMMIT; diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..2014c6e --- /dev/null +++ b/frontend/.env @@ -0,0 +1,9 @@ +VITE_ENABLE_MOCK=true +VITE_APP_PORT=5173 +VITE_SOURCE_MAP=true +# mock +VITE_BASE_URL=http://localhost:5173 +# local +#VITE_BASE_URL=http://localhost:8080 +# dev +#VITE_BASE_URL=https://localhost/api diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..47f6cf4 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,181 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +test-results/ +playwright-report/ + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.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 + +# 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 + +# Auto-generated type declarations +auto-imports.d.ts +components.d.ts diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 0000000..deed13c --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +lts/jod diff --git a/frontend/.vscode/extensions.json b/frontend/.vscode/extensions.json new file mode 100644 index 0000000..a8084df --- /dev/null +++ b/frontend/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "Vue.volar", + "vitest.explorer", + "ms-playwright.playwright" + ] +} diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 0000000..5187624 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "tsconfig.json": "tsconfig.*.json, env.d.ts", + "vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*", + "package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .prettier*, prettier*, .editorconfig" + }, + "files.associations": { + "*.css": "tailwindcss" + }, + "editor.quickSuggestions": { + "strings": "on" + }, + "tailwindCSS.classAttributes": ["class", "ui"], + "tailwindCSS.experimental.classRegex": [ + ["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"] + ] +} diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 0000000..e6b5818 --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": ["api/schema/**", "public"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noNonNullAssertion": "off", + "noUnusedTemplateLiteral": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/frontend/e2e/tsconfig.json b/frontend/e2e/tsconfig.json new file mode 100644 index 0000000..df0e963 --- /dev/null +++ b/frontend/e2e/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": ["./**/*"] +} diff --git a/frontend/e2e/vue.spec.ts b/frontend/e2e/vue.spec.ts new file mode 100644 index 0000000..d5d7765 --- /dev/null +++ b/frontend/e2e/vue.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from "@playwright/test"; + +// See here how to get started: +// https://playwright.dev/docs/intro +test("visits the app root url", async ({ page }) => { + await page.goto("/"); + await expect(page.locator("h1")).toHaveText("You did it!"); +}); diff --git a/frontend/env.d.ts b/frontend/env.d.ts new file mode 100644 index 0000000..af756f6 --- /dev/null +++ b/frontend/env.d.ts @@ -0,0 +1,25 @@ +/// +interface ViteTypeOptions { + // By adding this line, you can make the type of ImportMetaEnv strict + // to disallow unknown keys. + strictImportMetaEnv: unknown; +} + +interface ImportMetaEnv { + readonly VITE_ENABLE_MOCK: "true" | "false"; + readonly VITE_BACKEND_PORT: string; + readonly VITE_BASE_URL: string; + readonly VITE_SOURCE_MAP: "true" | "false"; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +interface AppConfig { + errorHandler?: ( + err: unknown, + instance: ComponentPublicInstance | null, + info: string, + ) => void; +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9e5fc8f --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..7437daf --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6449 @@ +{ + "name": "zhihu-frontend", + "version": "1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "zhihu-frontend", + "version": "1.0", + "dependencies": { + "@tailwindcss/vite": "^4.0.14", + "@vueuse/core": "^13.0.0", + "apexcharts": "^3.46.0", + "flowbite": "^3.1.2", + "openapi-fetch": "^0.13.5", + "pinia": "^3.0.1", + "tailwindcss": "^4.0.14", + "vue": "^3.5.13", + "vue-router": "^4.5.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@faker-js/faker": "^9.6.0", + "@playwright/test": "^1.51.0", + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.13.9", + "@vitejs/plugin-vue": "^5.2.1", + "@vitest/browser": "^3.0.9", + "@vue/tsconfig": "^0.7.0", + "msw": "^2.8.2", + "npm-run-all2": "^7.0.2", + "openapi-typescript": "^7.6.1", + "playwright": "^1.51.1", + "typescript": "~5.8.0", + "vite": "^6.2.1", + "vite-plugin-vue-devtools": "^7.7.2", + "vitest": "^3.0.8", + "vitest-browser-vue": "^0.2.0", + "vue-tsc": "^2.2.8" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.27.1.tgz", + "integrity": "sha512-DTxe4LBPrtFdsWzgpmbBKevg3e9PBy+dXRt19kSbucbZvL2uqtdqwwpluL1jfxYE0wIDTFp1nTy/q6gNLsxXrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", + "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@faker-js/faker": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.7.0.tgz", + "integrity": "sha512-aozo5vqjCmDoXLNUJarFZx2IN/GgGaogY4TMJ6so/WLZOWpSV7fvj2dmrV6sEAnUm1O7aCrhTibjpzeDFgNqbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", + "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", + "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", + "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.6", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", + "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "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/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.3.tgz", + "integrity": "sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "minimatch": "^5.0.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", + "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.29.2", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.6" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", + "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.6", + "@tailwindcss/oxide-darwin-arm64": "4.1.6", + "@tailwindcss/oxide-darwin-x64": "4.1.6", + "@tailwindcss/oxide-freebsd-x64": "4.1.6", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.6", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.6", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.6", + "@tailwindcss/oxide-linux-x64-musl": "4.1.6", + "@tailwindcss/oxide-wasm32-wasi": "4.1.6", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.6", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.6" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz", + "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz", + "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz", + "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz", + "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz", + "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz", + "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz", + "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz", + "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz", + "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz", + "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "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.9", + "@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.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz", + "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz", + "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.6.tgz", + "integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.6", + "@tailwindcss/oxide": "4.1.6", + "tailwindcss": "4.1.6" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tsconfig/node22": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.1.tgz", + "integrity": "sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "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.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/browser": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.1.3.tgz", + "integrity": "sha512-Dgyez9LbHJHl9ObZPo5mu4zohWLo7SMv8zRWclMF+dxhQjmOtEP0raEX13ac5ygcvihNoQPBZXdya5LMSbcCDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/user-event": "^14.6.1", + "@vitest/mocker": "3.1.3", + "@vitest/utils": "3.1.3", + "magic-string": "^0.30.17", + "sirv": "^3.0.1", + "tinyrainbow": "^2.0.0", + "ws": "^8.18.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "3.1.3", + "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", + "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.3", + "@vitest/utils": "3.1.3", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", + "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.3", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", + "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", + "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.1.3", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", + "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.3", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", + "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.3", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.13", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.13.tgz", + "integrity": "sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.13" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.13", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.13.tgz", + "integrity": "sha512-l/EBcc2FkvHgz2ZxV+OZK3kMSroMr7nN3sZLF2/f6kWW66q8+tEL4giiYyFjt0BcubqJhBt6soYIrAPhg/Yr+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.13", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.13.tgz", + "integrity": "sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.13", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz", + "integrity": "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.4.0.tgz", + "integrity": "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "@vue/babel-helper-vue-transform-on": "1.4.0", + "@vue/babel-plugin-resolve-type": "1.4.0", + "@vue/shared": "^3.5.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.4.0.tgz", + "integrity": "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/parser": "^7.26.9", + "@vue/compiler-sfc": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.6.tgz", + "integrity": "sha512-b2Xx0KvXZObePpXPYHvBRRJLDQn5nhKjXh7vUhMEtWxz1AYNFOVIsh5+HLP8xDGL7sy+Q7hXeUxPHB/KgbtsPw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.6" + } + }, + "node_modules/@vue/devtools-core": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.6.tgz", + "integrity": "sha512-ghVX3zjKPtSHu94Xs03giRIeIWlb9M+gvDRVpIZ/cRIxKHdW6HE/sm1PT3rUYS3aV92CazirT93ne+7IOvGUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.6", + "@vue/devtools-shared": "^7.7.6", + "mitt": "^3.0.1", + "nanoid": "^5.1.0", + "pathe": "^2.0.3", + "vite-hot-client": "^2.0.4" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-core/node_modules/nanoid": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.6.tgz", + "integrity": "sha512-geu7ds7tem2Y7Wz+WgbnbZ6T5eadOvozHZ23Atk/8tksHMFOFylKi1xgGlQlVn0wlkEf4hu+vd5ctj1G4kFtwA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.6", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.6.tgz", + "integrity": "sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.10.tgz", + "integrity": "sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~2.4.11", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.1.0.tgz", + "integrity": "sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "13.1.0", + "@vueuse/shared": "13.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.1.0.tgz", + "integrity": "sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.1.0.tgz", + "integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/apexcharts": { + "version": "3.54.1", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.1.tgz", + "integrity": "sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==", + "license": "MIT", + "dependencies": { + "@yr/monotone-cubic-spline": "^1.0.3", + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.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.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/birpc": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.3.0.tgz", + "integrity": "sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "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/cross-spawn/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/cross-spawn/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/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "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/editorconfig/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.151", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz", + "integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", + "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "9.5.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.3.tgz", + "integrity": "sha512-QFNnTvU3UjgWFy8Ef9iDHvIdcgZ344ebkwYx4/KLbR+CKQA4xBaHzv+iRpp86QfMHP8faFQLh8iOc57215y4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.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/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flowbite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-3.1.2.tgz", + "integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.3", + "flowbite-datepicker": "^1.3.1", + "mini-svg-data-uri": "^1.4.3", + "postcss": "^8.5.1" + } + }, + "node_modules/flowbite-datepicker": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.3.2.tgz", + "integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==", + "license": "MIT", + "dependencies": { + "@rollup/plugin-node-resolve": "^15.2.3", + "flowbite": "^2.0.0" + } + }, + "node_modules/flowbite-datepicker/node_modules/flowbite": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.5.2.tgz", + "integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.3", + "flowbite-datepicker": "^1.3.0", + "mini-svg-data-uri": "^1.4.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/index-to-position": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", + "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "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.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "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.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/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==", + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "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==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/msw": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.8.2.tgz", + "integrity": "sha512-ugu8RBgUj6//RD0utqDDPdS+QIs36BKYkDAM6u59hcMVtFM4PM0vW4l3G1R+1uCWP2EWFUG8reT/gPXVEtx7/w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-all2": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-7.0.2.tgz", + "integrity": "sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "minimatch": "^9.0.0", + "pidtree": "^0.6.0", + "read-package-json-fast": "^4.0.0", + "shell-quote": "^1.7.3", + "which": "^5.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0", + "npm": ">= 9" + } + }, + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-fetch": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.13.7.tgz", + "integrity": "sha512-3QdcAgvLFZ4j9Rcja7/Tq1j0qyv/LPqnHzwtMz5gKj6iddTdP48r+FZd8rmb+6xL9CdnJ5OmgdM0CpgGBi6Fgg==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, + "node_modules/openapi-typescript": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.7.1.tgz", + "integrity": "sha512-apa45jEOxcVfYHTD9eHpW0cR8yQn7jQFTCnSWNdjKbWmyjha8CMj1LdGHdk0Z70kFSR6EfjdDuOCY7PFICrNSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.2", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^9.4.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "license": "MIT" + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pinia": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.2.tgz", + "integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.2" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "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.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-package-json-fast": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", + "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "license": "MIT", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==", + "license": "MIT" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "license": "MIT", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", + "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "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==", + "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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "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/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.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==", + "dev": true, + "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/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "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-hot-client": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.0.4.tgz", + "integrity": "sha512-W9LOGAyGMrbGArYJN4LBCdOC5+Zwh7dHvOHC0KmGKkJhsOzaKbpo/jEjpPKVHIW0/jBWj8RZG0NUxfgA8BxgAg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0" + } + }, + "node_modules/vite-node": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", + "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.9.tgz", + "integrity": "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.3", + "debug": "^4.3.7", + "error-stack-parser-es": "^0.1.5", + "fs-extra": "^11.2.0", + "open": "^10.1.0", + "perfect-debounce": "^1.0.0", + "picocolors": "^1.1.1", + "sirv": "^3.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.7.6.tgz", + "integrity": "sha512-L7nPVM5a7lgit/Z+36iwoqHOaP3wxqVi1UvaDJwGCfblS9Y6vNqf32ILlzJVH9c47aHu90BhDXeZc+rgzHRHcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^7.7.6", + "@vue/devtools-kit": "^7.7.6", + "@vue/devtools-shared": "^7.7.6", + "execa": "^9.5.2", + "sirv": "^3.0.1", + "vite-plugin-inspect": "0.8.9", + "vite-plugin-vue-inspector": "^5.3.1" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.1.tgz", + "integrity": "sha512-cBk172kZKTdvGpJuzCCLg8lJ909wopwsu3Ve9FsL1XsnLBiRT9U3MePcqrgGHgCX2ZgkqZmAGR8taxw+TV6s7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", + "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.1.3", + "@vitest/mocker": "3.1.3", + "@vitest/pretty-format": "^3.1.3", + "@vitest/runner": "3.1.3", + "@vitest/snapshot": "3.1.3", + "@vitest/spy": "3.1.3", + "@vitest/utils": "3.1.3", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.13", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.3", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.3", + "@vitest/ui": "3.1.3", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest-browser-vue": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/vitest-browser-vue/-/vitest-browser-vue-0.2.0.tgz", + "integrity": "sha512-18v3uUQebbtSba2jbqcRvqfUCebd3f0nFA3BLKFZF9P0vnj7QgOm2JAxlcCA1ytNSnyzV3OtnNeNpThRaW6AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/test-utils": "^2.4.6" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "^2.1.0 || ^3.0.0-0", + "vitest": "^2.1.0 || ^3.0.0-0", + "vue": "^3.0.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.10.tgz", + "integrity": "sha512-iDUO7uQK+Sab2tYuiP9D1oLujCWlhHELHMgV/cB13cuGbG4qwkLHvtfWb6FzvxrIOPDnU0oHsz2MlQjhYDeaHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.10.tgz", + "integrity": "sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.2.10" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3e9c639 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,55 @@ +{ + "name": "zhihu-frontend", + "version": "1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "generate:api": "openapi-typescript ./src/api/schema/openapi.json -o ./src/api/types/schema.d.ts", + "format": "biome format --write .", + "lint": "biome lint --write .", + "check": "biome check --write .", + "test:unit": "vitest", + "test:e2e": "playwright test", + "test:ts": "tsc --noEmit", + "build-only": "vite build", + "type-check": "vue-tsc --build" + }, + "dependencies": { + "@tailwindcss/vite": "^4.0.14", + "@vueuse/core": "^13.0.0", + "apexcharts": "^3.46.0", + "flowbite": "^3.1.2", + "openapi-fetch": "^0.13.5", + "pinia": "^3.0.1", + "tailwindcss": "^4.0.14", + "vue": "^3.5.13", + "vue-router": "^4.5.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@faker-js/faker": "^9.6.0", + "@playwright/test": "^1.51.0", + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.13.9", + "@vitejs/plugin-vue": "^5.2.1", + "@vitest/browser": "^3.0.9", + "@vue/tsconfig": "^0.7.0", + "msw": "^2.8.2", + "npm-run-all2": "^7.0.2", + "openapi-typescript": "^7.6.1", + "playwright": "^1.51.1", + "typescript": "~5.8.0", + "vite": "^6.2.1", + "vite-plugin-vue-devtools": "^7.7.2", + "vitest": "^3.0.8", + "vitest-browser-vue": "^0.2.0", + "vue-tsc": "^2.2.8" + }, + "msw": { + "workerDirectory": ["public"] + } +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..4016283 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,111 @@ +import process from "node:process"; +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.CI ? "http://localhost:4173" : "http://localhost:5173", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + screenshot: "off", + video: "off", + /* Only on CI systems run the tests headless */ + headless: !!process.env.CI, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + }, + }, + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + }, + }, + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + webServer: { + /** + * Use the dev server by default for faster feedback loop. + * Use the preview server on CI for more realistic testing. + * Playwright will re-use the local server if there is already a dev-server running. + */ + command: process.env.CI ? "npm run preview" : "npm run dev", + port: process.env.CI ? 4173 : 5173, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js new file mode 100644 index 0000000..7500d41 --- /dev/null +++ b/frontend/public/mockServiceWorker.js @@ -0,0 +1,307 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.8.2' +const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/frontend/public/trump.jpg b/frontend/public/trump.jpg new file mode 100644 index 0000000..7cdbcb5 Binary files /dev/null and b/frontend/public/trump.jpg differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..58745fb --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..b0e2a78 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,69 @@ +import createClient, { type Middleware } from "openapi-fetch"; +import useAuthStore from "../composables/store/useAuthStore"; +import { + ForbiddenError, + SystemError, + UnAuthError, + InternalServerError, +} from "../types/error"; +import type { paths } from "./types/schema"; // generated by openapi-typescript + +const myMiddleware: Middleware = { + onRequest({ request, options }) { + const authStore = useAuthStore(); + request.headers.set("Authorization", authStore.get()); + return request; + }, + async onResponse({ request, response, options }) { + const { body, ...resOptions } = response; + if (response.status >= 400 && response.status < 500) { + if (response.status === 401) { + handleAuthError(response); + } else if (response.status === 403) { + handleForbiddenError(response); + } else { + handleSystemError(response); + } + } else if (response.status >= 500) { + await handleBusinessError(response); + } else { + return response; + } + }, + async onError({ error }) { + // wrap errors thrown by fetch + return; + }, +}; + +const client = createClient({ + baseUrl: `${import.meta.env.VITE_BASE_URL}`, + querySerializer: { + object: { + style: "form", + explode: true, + }, + }, +}); + +// register middleware +client.use(myMiddleware); + +const handleAuthError = (response: Response) => { + throw new UnAuthError(response.status); +}; + +const handleForbiddenError = (response: Response) => { + throw new ForbiddenError(response.status); +}; + +const handleSystemError = (response: Response) => { + throw new SystemError(response.status); +}; + +const handleBusinessError = async (response: Response) => { + const data = await response.json(); + throw new InternalServerError(response.status, data.detail); +}; + +export default client; diff --git a/frontend/src/api/mocks/authHandlers.ts b/frontend/src/api/mocks/authHandlers.ts new file mode 100644 index 0000000..277666a --- /dev/null +++ b/frontend/src/api/mocks/authHandlers.ts @@ -0,0 +1,10 @@ +import { faker } from "@faker-js/faker"; +import { http, HttpResponse } from "msw"; + +export default [ + http.post("/auth/sign-in", () => { + const response = HttpResponse.json(); + response.headers.set("Authorization", faker.string.alpha(16)); + return response; + }), +]; diff --git a/frontend/src/api/mocks/departmentHandlers.ts b/frontend/src/api/mocks/departmentHandlers.ts new file mode 100644 index 0000000..674f99a --- /dev/null +++ b/frontend/src/api/mocks/departmentHandlers.ts @@ -0,0 +1,39 @@ +import { faker } from "@faker-js/faker"; +import { http, HttpResponse } from "msw"; + +export default [ + http.get("/department/page-query", () => { + const generateDepartment = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + name: faker.company.name(), + parentId: faker.number.int({ min: 1, max: 100 }), + isBound: faker.datatype.boolean(), + parentName: faker.company.name(), + }); + const mockData = { + data: faker.helpers.multiple(generateDepartment, { count: 10 }), + total: 30, + }; + return HttpResponse.json(mockData); + }), + http.get("/department/query", () => { + const generateDepartment = () => ({ + id: faker.number.int({ min: 1, max: 30 }), + name: faker.company.name(), + parentId: faker.number.int({ min: 1, max: 30 }), + parentName: faker.company.name(), + }); + const mockData = faker.helpers.multiple(generateDepartment, { count: 30 }); + + return HttpResponse.json(mockData); + }), + + http.post("/department", () => { + console.log("Captured department upsert"); + return HttpResponse.json(); + }), + http.delete("/department", () => { + console.log("Captured department delete"); + return HttpResponse.json(); + }), +]; diff --git a/frontend/src/api/mocks/permissionHandlers.ts b/frontend/src/api/mocks/permissionHandlers.ts new file mode 100644 index 0000000..873d603 --- /dev/null +++ b/frontend/src/api/mocks/permissionHandlers.ts @@ -0,0 +1,43 @@ +import { faker } from "@faker-js/faker"; +import { http, HttpResponse } from "msw"; + +export default [ + http.get("/iam/permissions", () => { + const generatePermission = () => ({ + id: faker.number.int({ min: 1, max: 20 }), + code: `perm_${faker.lorem.words({ min: 1, max: 1 })}`, + name: faker.lorem.words({ min: 1, max: 1 }), + isBound: faker.datatype.boolean(), + }); + + const mockData = { + data: faker.helpers.multiple(generatePermission, { count: 10 }), + total: 20, + }; + return HttpResponse.json(mockData); + }), + + http.post("/iam/permission", async ({ request }) => { + console.log('Captured a "POST /posts" request'); + return HttpResponse.json(); + }), + + http.delete("/iam/permission", ({ params }) => { + console.log(`Captured a "DELETE /posts/${params.id}" request`); + return HttpResponse.json(); + }), + + http.post("/iam/roles/:roleId/bind-permission", ({ params, request }) => { + console.log( + `Captured a "POST /urp/roles/${params.roleId}/bind-permission" request`, + ); + return HttpResponse.json(); + }), + + http.post("/iam/roles/:roleId/unbind-permission", ({ params, request }) => { + console.log( + `Captured a "POST /urp/roles/${params.roleId}/unbind-permission" request`, + ); + return HttpResponse.json(); + }), +]; diff --git a/frontend/src/api/mocks/positionHandlers.ts b/frontend/src/api/mocks/positionHandlers.ts new file mode 100644 index 0000000..04a08d1 --- /dev/null +++ b/frontend/src/api/mocks/positionHandlers.ts @@ -0,0 +1,35 @@ +import { faker } from "@faker-js/faker"; +import { http, HttpResponse } from "msw"; + +export default [ + http.get("/position/page-query", () => { + const generatePosition = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + name: faker.person.jobTitle(), + isBound: faker.datatype.boolean(), + }); + const mockData = { + data: faker.helpers.multiple(generatePosition, { count: 10 }), + total: 30, + }; + return HttpResponse.json(mockData); + }), + http.get("/position/query", () => { + const generatePosition = () => ({ + id: faker.number.int({ min: 1, max: 30 }), + name: faker.person.jobTitle(), + }); + const mockData = faker.helpers.multiple(generatePosition, { count: 30 }); + + return HttpResponse.json(mockData); + }), + + http.post("/position", () => { + console.log("Captured position upsert"); + return HttpResponse.json(); + }), + http.delete("/position", () => { + console.log("Captured position delete"); + return HttpResponse.json(); + }), +]; diff --git a/frontend/src/api/mocks/roleHandlers.ts b/frontend/src/api/mocks/roleHandlers.ts new file mode 100644 index 0000000..1b69b23 --- /dev/null +++ b/frontend/src/api/mocks/roleHandlers.ts @@ -0,0 +1,77 @@ +import { faker } from "@faker-js/faker"; +import { http, HttpResponse } from "msw"; + +export default [ + http.get("/iam/roles", () => { + const generatePermission = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: `perm_${faker.lorem.word()}`, + name: faker.lorem.words({ min: 1, max: 3 }), + }); + + const generateRole = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: faker.helpers.arrayElement([ + "admin", + "editor", + "viewer", + "manager", + ]), + name: faker.person.jobTitle(), + isBound: faker.datatype.boolean(), + permissions: faker.helpers.multiple(generatePermission, { + count: { min: 1, max: 5 }, + }), + }); + + const mockData = { + data: faker.helpers.multiple(generateRole, { count: 10 }), + total: 20, + }; + + return HttpResponse.json(mockData); + }), + http.get("/iam/role", ({ params }) => { + const generatePermission = () => ({ + id: faker.number.int({ min: 1, max: 10 }), + code: `perm_${faker.lorem.word()}`, + name: faker.lorem.words({ min: 1, max: 3 }), + }); + + const generateRole = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: faker.helpers.arrayElement([ + "admin", + "editor", + "viewer", + "manager", + ]), + name: faker.person.jobTitle(), + permissions: faker.helpers.multiple(generatePermission, { + count: { min: 1, max: 5 }, + }), + }); + + return HttpResponse.json(generateRole()); + }), + + http.post("/iam/role", async ({ request }) => { + console.log('Captured a "POST /urp/role" request'); + return HttpResponse.json(); + }), + + http.post("/iam/permission/bind", async ({ request }) => { + console.log('Captured a "POST /iam/permission/bind" request'); + return HttpResponse.json(); + }), + + http.post("/iam/permission/unbind", async ({ request }) => { + console.log('Captured a "POST /iam/permission/unbind" request'); + return HttpResponse.json(); + }), + + http.delete("/iam/role", ({ params }) => { + console.log(`Captured a "DELETE /urp/role ${params.id}" request`); + return HttpResponse.json(); + }), +]; diff --git a/frontend/src/api/mocks/schedulerHandlers.ts b/frontend/src/api/mocks/schedulerHandlers.ts new file mode 100644 index 0000000..381257e --- /dev/null +++ b/frontend/src/api/mocks/schedulerHandlers.ts @@ -0,0 +1,53 @@ +import { faker } from "@faker-js/faker"; +import { http, HttpResponse } from "msw"; + +export default [ + http.get("/scheduler/page-query", () => { + const generateJobs = () => ({ + name: faker.word.sample(), + group: faker.helpers.arrayElement(["default", "system", "custom"]), + className: `com.example.jobs.${faker.word.sample()}Job`, + jobDataMap: { + dirty: faker.datatype.boolean(), + allowsTransientData: faker.datatype.boolean(), + keys: faker.helpers.multiple(() => faker.word.sample(), { count: 3 }), + empty: false, + wrappedMap: {}, + }, + triggerName: faker.word.sample(), + triggerGroup: faker.helpers.arrayElement(["DEFAULT", "SYSTEM"]), + schedulerType: faker.helpers.arrayElement(["CRON", "SIMPLE"]), + triggerState: faker.helpers.arrayElement(["PAUSE", "WAITING"]), + cronExpression: "0 0/30 * * * ?", + startTime: faker.date.past().getTime(), + endTime: faker.date.future().getTime(), + nextFireTime: faker.date.soon().getTime(), + previousFireTime: faker.date.recent().getTime(), + triggerJobDataMap: { + dirty: faker.datatype.boolean(), + allowsTransientData: true, + keys: [], + empty: true, + wrappedMap: {}, + }, + }); + + const mockData = { + data: faker.helpers.multiple(generateJobs, { count: 20 }), + total: 20, + }; + return HttpResponse.json(mockData); + }), + http.post("/scheduler/trigger/resume", () => { + console.log('Captured a "POST /scheduler/trigger/resume" request'); + return HttpResponse.json(); + }), + http.post("/scheduler/trigger/pause", () => { + console.log('Captured a "POST /scheduler/trigger/pause" request'); + return HttpResponse.json(); + }), + http.put("/scheduler/job/update", () => { + console.log('Captured a "POST /scheduler/job/update" request'); + return HttpResponse.json(); + }), +]; diff --git a/frontend/src/api/mocks/setup.ts b/frontend/src/api/mocks/setup.ts new file mode 100644 index 0000000..0209acb --- /dev/null +++ b/frontend/src/api/mocks/setup.ts @@ -0,0 +1,17 @@ +import { setupWorker } from "msw/browser"; +import authHandlers from "./authHandlers"; +import jobHandlers from "./schedulerHandlers"; +import permissionHandlers from "./permissionHandlers"; +import roleHandlers from "./roleHandlers"; +import userHandlers from "./userHandlers"; +import departmentHandlers from "./departmentHandlers"; +import positionHandlers from "./positionHandlers"; +export const worker = setupWorker( + ...userHandlers, + ...authHandlers, + ...roleHandlers, + ...permissionHandlers, + ...jobHandlers, + ...departmentHandlers, + ...positionHandlers, +); diff --git a/frontend/src/api/mocks/userHandlers.ts b/frontend/src/api/mocks/userHandlers.ts new file mode 100644 index 0000000..d13d8c3 --- /dev/null +++ b/frontend/src/api/mocks/userHandlers.ts @@ -0,0 +1,188 @@ +import { faker } from "@faker-js/faker"; +import { http, HttpResponse } from "msw"; +import { ROLE } from "../../router/constants"; + +export default [ + http.get("/iam/user", () => { + const generatePermission = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: `perm_${faker.lorem.words({ min: 1, max: 1 })}`, + name: faker.lorem.words({ min: 1, max: 1 }), + }); + + const generateRole = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: faker.helpers.arrayElement([ + ROLE.ADMIN, + "editor", + "viewer", + "manager", + ]), + name: faker.person.jobTitle(), + permissions: faker.helpers.multiple(generatePermission, { + count: { min: 1, max: 5 }, + }), + }); + + const generateDepartment = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: `dept_${faker.lorem.word()}`, + name: faker.company.name(), + parentId: faker.number.int({ min: 1, max: 30 }), + enable: faker.datatype.boolean(), + }); + + const generateUser = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + username: faker.internet.email(), + password: faker.internet.password(), + enable: faker.datatype.boolean(), + roles: faker.helpers.multiple(generateRole, { + count: { min: 1, max: 3 }, + }), + createTime: faker.date.recent({ days: 30 }).toISOString(), + permissions: faker.helpers.multiple(generatePermission, { + count: { min: 1, max: 5 }, + }), + departments: faker.helpers.multiple(generateDepartment, { + count: { min: 0, max: 3 }, + }), + }); + + return HttpResponse.json(generateUser()); + }), + http.get("/iam/users", () => { + const generatePermission = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: `perm_${faker.lorem.word()}`, + name: faker.lorem.words({ min: 1, max: 3 }), + }); + + const generateRole = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: [ROLE.ADMIN, "editor", "viewer", "manager"], + name: faker.person.jobTitle(), + permissions: faker.helpers.multiple(generatePermission, { + count: { min: 1, max: 5 }, + }), + }); + + const generateDepartment = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: `dept_${faker.lorem.word()}`, + name: faker.company.name(), + parentId: faker.number.int({ min: 1, max: 30 }), + enable: faker.datatype.boolean(), + }); + + const generateUser = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + username: faker.internet.email(), + password: faker.internet.password(), + enable: faker.datatype.boolean(), + roles: faker.helpers.multiple(generateRole, { + count: { min: 1, max: 3 }, + }), + createTime: faker.date.recent({ days: 30 }).toISOString(), + permissions: faker.helpers.multiple(generatePermission, { + count: { min: 1, max: 5 }, + }), + departments: faker.helpers.multiple(generateDepartment, { + count: { min: 0, max: 3 }, + }), + }); + + const mockData = { + data: faker.helpers.multiple(generateUser, { count: 10 }), + total: 30, + }; + return HttpResponse.json(mockData); + }), + http.post("/api/users/:userId/departments", () => { + console.log('Captured a "POST /api/users/:userId/departments" request'); + return HttpResponse.json({ success: true }); + }), + http.delete("/api/users/:userId/departments", () => { + console.log('Captured a "DELETE /api/users/:userId/departments" request'); + return HttpResponse.json({ success: true }); + }), + http.post("/iam/user", () => { + console.log('Captured a "POST /posts" request'); + return HttpResponse.json(); + }), + http.delete("/iam/user", ({ params }) => { + console.log(`Captured a "DELETE /posts/${params.id}" request`); + return HttpResponse.json(); + }), + http.post("/iam/me", () => { + console.log('Captured a "POST /posts" request'); + return HttpResponse.json(); + }), + http.post("/iam/role/bind", () => { + console.log('Captured a "POST /posts" request'); + return HttpResponse.json(); + }), + http.post("/iam/role/unbind", () => { + console.log('Captured a "POST /posts" request'); + return HttpResponse.json(); + }), + http.get("/iam/me", () => { + const generatePermission = () => ({ + id: faker.number.int({ min: 1, max: 1000 }), + code: `perm_${faker.lorem.word()}`, + name: faker.lorem.words({ min: 1, max: 3 }), + }); + + const generateRole = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: [ROLE.ADMIN, "editor", "viewer", "manager"], + name: faker.person.jobTitle(), + permissions: faker.helpers.multiple(generatePermission, { + count: { min: 1, max: 5 }, + }), + }); + + const generateDepartment = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + code: `dept_${faker.lorem.word()}`, + name: faker.company.name(), + parentId: faker.number.int({ min: 1, max: 30 }), + enable: faker.datatype.boolean(), + }); + + const generateUser = () => ({ + id: faker.number.int({ min: 1, max: 100 }), + username: faker.internet.email(), + password: faker.internet.password(), + enable: faker.datatype.boolean(), + roles: faker.helpers.multiple(generateRole, { + count: { min: 1, max: 3 }, + }), + createTime: faker.date.recent({ days: 30 }).toISOString(), + permissions: faker.helpers.multiple(generatePermission, { + count: { min: 1, max: 5 }, + }), + departments: faker.helpers.multiple(generateDepartment, { + count: { min: 0, max: 3 }, + }), + }); + const mockData = generateUser(); + return HttpResponse.json(mockData); + }), + http.post("/department/unbind", () => { + console.log("Captured a 'POST /department/unbind' request"); + return HttpResponse.json(); + }), + http.post("/department/bind", () => { + console.log("Captured a 'POST /department/bind' request"); + return HttpResponse.json(); + }), + http.post("/iam/position/bind", () => { + console.log('Captured a "POST /posts" request'); + return HttpResponse.json(); + }), + http.post("/iam/position/unbind", () => { + console.log('Captured a "POST /posts" request'); + return HttpResponse.json(); + }), +]; diff --git a/frontend/src/api/schema/openapi.json b/frontend/src/api/schema/openapi.json new file mode 100644 index 0000000..9bc68c8 --- /dev/null +++ b/frontend/src/api/schema/openapi.json @@ -0,0 +1,1651 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "paths": { + "/scheduler/job/update": { + "put": { + "tags": [ + "scheduler-controller" + ], + "operationId": "updateJob", + "parameters": [ + { + "name": "cron", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TriggerKeyDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/scheduler/trigger/resume": { + "post": { + "tags": [ + "scheduler-controller" + ], + "operationId": "resumeTrigger", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TriggerKeyDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/scheduler/trigger/pause": { + "post": { + "tags": [ + "scheduler-controller" + ], + "operationId": "pauseTrigger", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TriggerKeyDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/scheduler/job/trigger": { + "post": { + "tags": [ + "scheduler-controller" + ], + "operationId": "triggerJob", + "parameters": [ + { + "name": "startAt", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobKeyDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/position": { + "post": { + "tags": [ + "position-controller" + ], + "operationId": "upsertPosition", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Position" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "position-controller" + ], + "operationId": "deletePosition", + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/user": { + "get": { + "tags": [ + "identity-access-controller" + ], + "operationId": "queryUserWithRolePermission", + "parameters": [ + { + "name": "userId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserRolePermissionDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "upsertUser", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "identity-access-controller" + ], + "operationId": "deleteUser", + "parameters": [ + { + "name": "userId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/role": { + "get": { + "tags": [ + "identity-access-controller" + ], + "operationId": "queryRoleWithPermission", + "parameters": [ + { + "name": "roleId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RoleDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "upsertRole", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "identity-access-controller" + ], + "operationId": "deleteRole", + "parameters": [ + { + "name": "roleId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/role/unbind": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "unBindRoleBy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleBindDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/role/bind": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "bindRoleBy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleBindDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/position/unbind": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "unBindPositionBy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PositionBindDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/position/bind": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "bindPositionBy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PositionBindDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/permission": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "upsertPermission", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "identity-access-controller" + ], + "operationId": "deletePermission", + "parameters": [ + { + "name": "permissionId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/permission/unbind": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "unBindPermissionBy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionBindDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/permission/bind": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "bindPermissionBy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionBindDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/me": { + "get": { + "tags": [ + "identity-access-controller" + ], + "operationId": "currentUser", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserRolePermissionDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "upsertMe", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/department/unbind": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "unBindDepartmentBy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DepartmentBindDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/iam/department/bind": { + "post": { + "tags": [ + "identity-access-controller" + ], + "operationId": "bindDepartmentBy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DepartmentBindDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/department": { + "post": { + "tags": [ + "department-controller" + ], + "operationId": "upsertDepartment", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Department" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "department-controller" + ], + "operationId": "deleteDepartment", + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/sign-up": { + "post": { + "tags": [ + "sign-controller" + ], + "operationId": "signUp", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignUpDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/auth/sign-out": { + "post": { + "tags": [ + "sign-controller" + ], + "operationId": "signOut", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/auth/sign-in": { + "post": { + "tags": [ + "sign-controller" + ], + "operationId": "signIn", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignInDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/scheduler/page-query": { + "get": { + "tags": [ + "scheduler-controller" + ], + "operationId": "pageQuery", + "parameters": [ + { + "name": "pageRequestDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PageRequestDto" + } + }, + { + "name": "queryDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/QueryDto" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResponseDtoListJobTriggerDto" + } + } + } + } + } + } + }, + "/position/query": { + "get": { + "tags": [ + "position-controller" + ], + "operationId": "queryPositions", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Position" + } + } + } + } + } + } + } + }, + "/position/page-query": { + "get": { + "tags": [ + "position-controller" + ], + "operationId": "pageQueryPositions", + "parameters": [ + { + "name": "pageRequestDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PageRequestDto" + } + }, + { + "name": "positionQueryDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PositionQueryDto" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResponseDtoListPositionRespDto" + } + } + } + } + } + } + }, + "/iam/users": { + "get": { + "tags": [ + "identity-access-controller" + ], + "operationId": "queryUsers", + "parameters": [ + { + "name": "pageRequestDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PageRequestDto" + } + }, + { + "name": "userQueryDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/UserQueryDto" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResponseDtoListUserRolePermissionDto" + } + } + } + } + } + } + }, + "/iam/roles": { + "get": { + "tags": [ + "identity-access-controller" + ], + "operationId": "queryRoles", + "parameters": [ + { + "name": "pageRequestDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PageRequestDto" + } + }, + { + "name": "roleQueryDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/RoleQueryDto" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResponseDtoListRoleDto" + } + } + } + } + } + } + }, + "/iam/permissions": { + "get": { + "tags": [ + "identity-access-controller" + ], + "operationId": "queryPermissions", + "parameters": [ + { + "name": "pageRequestDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PageRequestDto" + } + }, + { + "name": "permissionQueryDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PermissionQueryDto" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResponseDtoListPermissionRespDto" + } + } + } + } + } + } + }, + "/department/query": { + "get": { + "tags": [ + "department-controller" + ], + "operationId": "queryDepartments", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Department" + } + } + } + } + } + } + } + }, + "/department/page-query": { + "get": { + "tags": [ + "department-controller" + ], + "operationId": "pageQueryDepartments", + "parameters": [ + { + "name": "pageRequestDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PageRequestDto" + } + }, + { + "name": "departmentQueryDto", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/DepartmentQueryDto" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResponseDtoListDepartmentRespDto" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "TriggerKeyDto": { + "required": [ + "group", + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "group": { + "type": "string" + } + } + }, + "JobKeyDto": { + "required": [ + "group", + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "group": { + "type": "string" + } + } + }, + "Position": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + } + }, + "UserUpsertDto": { + "required": [ + "enable", + "username" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "enable": { + "type": "boolean" + } + } + }, + "RoleUpsertDto": { + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "RoleBindDto": { + "required": [ + "roleIds", + "userId" + ], + "type": "object", + "properties": { + "userId": { + "type": "integer", + "format": "int64" + }, + "roleIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "PositionBindDto": { + "required": [ + "positionIds", + "userId" + ], + "type": "object", + "properties": { + "userId": { + "type": "integer", + "format": "int64" + }, + "positionIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "PermissionUpsertDto": { + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "PermissionBindDto": { + "required": [ + "permissionIds", + "roleId" + ], + "type": "object", + "properties": { + "roleId": { + "type": "integer", + "format": "int64" + }, + "permissionIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "DepartmentBindDto": { + "required": [ + "departmentIds", + "userId" + ], + "type": "object", + "properties": { + "userId": { + "type": "integer", + "format": "int64" + }, + "departmentIds": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + } + } + }, + "Department": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "integer", + "format": "int64" + } + } + }, + "SignUpDto": { + "required": [ + "password", + "username" + ], + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "SignInDto": { + "required": [ + "password", + "username" + ], + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "PageRequestDto": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "format": "int64" + }, + "size": { + "type": "integer", + "format": "int64" + }, + "sortBy": { + "type": "object", + "additionalProperties": { + "type": "string", + "enum": [ + "ASC", + "DESC" + ] + } + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "sortFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SortFieldObject" + } + } + } + }, + "SortFieldObject": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "order": { + "type": "string", + "enum": [ + "ASC", + "DESC", + "DEFAULT" + ] + } + } + }, + "QueryDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "JobTriggerDto": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "group": { + "type": "string" + }, + "className": { + "type": "string" + }, + "jobDataMap": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "triggerName": { + "type": "string" + }, + "triggerGroup": { + "type": "string" + }, + "schedulerType": { + "type": "string" + }, + "cronExpression": { + "type": "string" + }, + "startTime": { + "type": "integer", + "format": "int64" + }, + "endTime": { + "type": "integer", + "format": "int64" + }, + "nextFireTime": { + "type": "integer", + "format": "int64" + }, + "previousFireTime": { + "type": "integer", + "format": "int64" + }, + "triggerState": { + "type": "string" + }, + "triggerJobDataMap": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + }, + "PageResponseDtoListJobTriggerDto": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/JobTriggerDto" + } + } + } + }, + "PositionQueryDto": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "bindState": { + "type": "string", + "enum": [ + "BIND", + "UNBIND", + "ALL" + ] + } + } + }, + "PageResponseDtoListPositionRespDto": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PositionRespDto" + } + } + } + }, + "PositionRespDto": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "integer", + "format": "int64" + }, + "isBound": { + "type": "boolean" + } + } + }, + "UserQueryDto": { + "type": "object", + "properties": { + "username": { + "type": "string" + } + } + }, + "PageResponseDtoListUserRolePermissionDto": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRolePermissionDto" + } + } + } + }, + "PermissionRespDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "isBound": { + "type": "boolean" + } + } + }, + "RoleDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "isBound": { + "type": "boolean" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionRespDto" + } + } + } + }, + "UserRolePermissionDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string", + "writeOnly": true + }, + "enable": { + "type": "boolean" + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleDto" + } + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "permissions": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionRespDto" + } + } + } + }, + "RoleQueryDto": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "format": "int64" + }, + "roleId": { + "type": "integer", + "format": "int64" + }, + "roleCode": { + "type": "string" + }, + "roleName": { + "type": "string" + }, + "roleIdList": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "bindState": { + "type": "string", + "enum": [ + "BIND", + "UNBIND", + "ALL" + ] + } + } + }, + "PageResponseDtoListRoleDto": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleDto" + } + } + } + }, + "PermissionQueryDto": { + "type": "object", + "properties": { + "roleId": { + "type": "integer", + "format": "int64" + }, + "permissionId": { + "type": "integer", + "format": "int64" + }, + "permissionCode": { + "type": "string" + }, + "permissionName": { + "type": "string" + }, + "permissionIdList": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" + } + }, + "bindState": { + "type": "string", + "enum": [ + "BIND", + "UNBIND", + "ALL" + ] + } + } + }, + "PageResponseDtoListPermissionRespDto": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionRespDto" + } + } + } + }, + "DepartmentQueryDto": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "bindState": { + "type": "string", + "enum": [ + "BIND", + "UNBIND", + "ALL" + ] + } + } + }, + "DepartmentRespDto": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "integer", + "format": "int64" + }, + "parentName": { + "type": "string" + }, + "isBound": { + "type": "boolean" + } + } + }, + "PageResponseDtoListDepartmentRespDto": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DepartmentRespDto" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/src/api/types/schema.d.ts b/frontend/src/api/types/schema.d.ts new file mode 100644 index 0000000..f72ffc4 --- /dev/null +++ b/frontend/src/api/types/schema.d.ts @@ -0,0 +1,1512 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/scheduler/job/update": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put: operations["updateJob"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/scheduler/trigger/resume": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["resumeTrigger"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/scheduler/trigger/pause": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["pauseTrigger"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/scheduler/job/trigger": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["triggerJob"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/position": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["upsertPosition"]; + delete: operations["deletePosition"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/user": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryUserWithRolePermission"]; + put?: never; + post: operations["upsertUser"]; + delete: operations["deleteUser"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/role": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryRoleWithPermission"]; + put?: never; + post: operations["upsertRole"]; + delete: operations["deleteRole"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/role/unbind": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["unBindRoleBy"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/role/bind": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["bindRoleBy"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/position/unbind": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["unBindPositionBy"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/position/bind": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["bindPositionBy"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/permission": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["upsertPermission"]; + delete: operations["deletePermission"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/permission/unbind": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["unBindPermissionBy"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/permission/bind": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["bindPermissionBy"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["currentUser"]; + put?: never; + post: operations["upsertMe"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/department/unbind": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["unBindDepartmentBy"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/department/bind": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["bindDepartmentBy"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/department": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["upsertDepartment"]; + delete: operations["deleteDepartment"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/sign-up": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["signUp"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/sign-out": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["signOut"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/sign-in": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["signIn"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/scheduler/page-query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["pageQuery"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/position/query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryPositions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/position/page-query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["pageQueryPositions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryUsers"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/roles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryRoles"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/iam/permissions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryPermissions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/department/query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["queryDepartments"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/department/page-query": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["pageQueryDepartments"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + TriggerKeyDto: { + name: string; + group: string; + }; + JobKeyDto: { + name: string; + group: string; + }; + Position: { + /** Format: int64 */ + id?: number; + name?: string; + }; + UserUpsertDto: { + /** Format: int64 */ + id?: number; + username: string; + password?: string; + enable: boolean; + }; + RoleUpsertDto: { + /** Format: int64 */ + id?: number; + code: string; + name: string; + }; + RoleBindDto: { + /** Format: int64 */ + userId: number; + roleIds: number[]; + }; + PositionBindDto: { + /** Format: int64 */ + userId: number; + positionIds: number[]; + }; + PermissionUpsertDto: { + /** Format: int64 */ + id?: number; + code: string; + name: string; + }; + PermissionBindDto: { + /** Format: int64 */ + roleId: number; + permissionIds: number[]; + }; + DepartmentBindDto: { + /** Format: int64 */ + userId: number; + departmentIds: number[]; + }; + Department: { + /** Format: int64 */ + id?: number; + name?: string; + /** Format: int64 */ + parentId?: number; + }; + SignUpDto: { + username: string; + password: string; + }; + SignInDto: { + username: string; + password: string; + }; + PageRequestDto: { + /** Format: int64 */ + page?: number; + /** Format: int64 */ + size?: number; + sortBy?: { + [key: string]: "ASC" | "DESC"; + }; + /** Format: int64 */ + offset?: number; + sortFields?: components["schemas"]["SortFieldObject"][]; + }; + SortFieldObject: { + name?: string; + /** @enum {string} */ + order?: "ASC" | "DESC" | "DEFAULT"; + }; + QueryDto: { + name?: string; + }; + JobTriggerDto: { + name?: string; + group?: string; + className?: string; + jobDataMap?: { + [key: string]: Record; + }; + triggerName?: string; + triggerGroup?: string; + schedulerType?: string; + cronExpression?: string; + /** Format: int64 */ + startTime?: number; + /** Format: int64 */ + endTime?: number; + /** Format: int64 */ + nextFireTime?: number; + /** Format: int64 */ + previousFireTime?: number; + triggerState?: string; + triggerJobDataMap?: { + [key: string]: Record; + }; + }; + PageResponseDtoListJobTriggerDto: { + /** Format: int64 */ + total?: number; + data?: components["schemas"]["JobTriggerDto"][]; + }; + PositionQueryDto: { + /** Format: int64 */ + userId?: number; + name?: string; + /** @enum {string} */ + bindState?: "BIND" | "UNBIND" | "ALL"; + }; + PageResponseDtoListPositionRespDto: { + /** Format: int64 */ + total?: number; + data?: components["schemas"]["PositionRespDto"][]; + }; + PositionRespDto: { + /** Format: int64 */ + id: number; + name: string; + /** Format: int64 */ + parentId?: number; + isBound?: boolean; + }; + UserQueryDto: { + username?: string; + }; + PageResponseDtoListUserRolePermissionDto: { + /** Format: int64 */ + total?: number; + data?: components["schemas"]["UserRolePermissionDto"][]; + }; + PermissionRespDto: { + /** Format: int64 */ + id?: number; + code?: string; + name?: string; + isBound?: boolean; + }; + RoleDto: { + /** Format: int64 */ + id?: number; + code?: string; + name?: string; + isBound?: boolean; + permissions?: components["schemas"]["PermissionRespDto"][]; + }; + UserRolePermissionDto: { + /** Format: int64 */ + id?: number; + username?: string; + password?: string; + enable?: boolean; + roles?: components["schemas"]["RoleDto"][]; + /** Format: date-time */ + createTime?: string; + permissions?: components["schemas"]["PermissionRespDto"][]; + }; + RoleQueryDto: { + /** Format: int64 */ + userId?: number; + /** Format: int64 */ + roleId?: number; + roleCode?: string; + roleName?: string; + roleIdList?: number[]; + /** @enum {string} */ + bindState?: "BIND" | "UNBIND" | "ALL"; + }; + PageResponseDtoListRoleDto: { + /** Format: int64 */ + total?: number; + data?: components["schemas"]["RoleDto"][]; + }; + PermissionQueryDto: { + /** Format: int64 */ + roleId?: number; + /** Format: int64 */ + permissionId?: number; + permissionCode?: string; + permissionName?: string; + permissionIdList?: number[]; + /** @enum {string} */ + bindState?: "BIND" | "UNBIND" | "ALL"; + }; + PageResponseDtoListPermissionRespDto: { + /** Format: int64 */ + total?: number; + data?: components["schemas"]["PermissionRespDto"][]; + }; + DepartmentQueryDto: { + /** Format: int64 */ + userId?: number; + name?: string; + enable?: boolean; + /** @enum {string} */ + bindState?: "BIND" | "UNBIND" | "ALL"; + }; + DepartmentRespDto: { + /** Format: int64 */ + id: number; + name: string; + /** Format: int64 */ + parentId?: number; + parentName?: string; + isBound?: boolean; + }; + PageResponseDtoListDepartmentRespDto: { + /** Format: int64 */ + total?: number; + data?: components["schemas"]["DepartmentRespDto"][]; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + updateJob: { + parameters: { + query: { + cron: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TriggerKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + resumeTrigger: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TriggerKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + pauseTrigger: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TriggerKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + triggerJob: { + parameters: { + query: { + startAt: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["JobKeyDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + upsertPosition: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Position"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deletePosition: { + parameters: { + query: { + id: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + queryUserWithRolePermission: { + parameters: { + query: { + userId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["UserRolePermissionDto"]; + }; + }; + }; + }; + upsertUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UserUpsertDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteUser: { + parameters: { + query: { + userId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + queryRoleWithPermission: { + parameters: { + query: { + roleId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["RoleDto"]; + }; + }; + }; + }; + upsertRole: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RoleUpsertDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteRole: { + parameters: { + query: { + roleId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + unBindRoleBy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RoleBindDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + bindRoleBy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RoleBindDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + unBindPositionBy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PositionBindDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + bindPositionBy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PositionBindDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + upsertPermission: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PermissionUpsertDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deletePermission: { + parameters: { + query: { + permissionId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + unBindPermissionBy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PermissionBindDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + bindPermissionBy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PermissionBindDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + currentUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["UserRolePermissionDto"]; + }; + }; + }; + }; + upsertMe: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UserUpsertDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + unBindDepartmentBy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DepartmentBindDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + bindDepartmentBy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DepartmentBindDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + upsertDepartment: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Department"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + deleteDepartment: { + parameters: { + query: { + id: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + signUp: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SignUpDto"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + signOut: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + signIn: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SignInDto"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + pageQuery: { + parameters: { + query: { + pageRequestDto: components["schemas"]["PageRequestDto"]; + queryDto: components["schemas"]["QueryDto"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["PageResponseDtoListJobTriggerDto"]; + }; + }; + }; + }; + queryPositions: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["Position"][]; + }; + }; + }; + }; + pageQueryPositions: { + parameters: { + query: { + pageRequestDto: components["schemas"]["PageRequestDto"]; + positionQueryDto: components["schemas"]["PositionQueryDto"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["PageResponseDtoListPositionRespDto"]; + }; + }; + }; + }; + queryUsers: { + parameters: { + query: { + pageRequestDto: components["schemas"]["PageRequestDto"]; + userQueryDto: components["schemas"]["UserQueryDto"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["PageResponseDtoListUserRolePermissionDto"]; + }; + }; + }; + }; + queryRoles: { + parameters: { + query: { + pageRequestDto: components["schemas"]["PageRequestDto"]; + roleQueryDto: components["schemas"]["RoleQueryDto"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["PageResponseDtoListRoleDto"]; + }; + }; + }; + }; + queryPermissions: { + parameters: { + query: { + pageRequestDto: components["schemas"]["PageRequestDto"]; + permissionQueryDto: components["schemas"]["PermissionQueryDto"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["PageResponseDtoListPermissionRespDto"]; + }; + }; + }; + }; + queryDepartments: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["Department"][]; + }; + }; + }; + }; + pageQueryDepartments: { + parameters: { + query: { + pageRequestDto: components["schemas"]["PageRequestDto"]; + departmentQueryDto: components["schemas"]["DepartmentQueryDto"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["PageResponseDtoListDepartmentRespDto"]; + }; + }; + }; + }; +} diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css new file mode 100644 index 0000000..cc7d014 --- /dev/null +++ b/frontend/src/assets/base.css @@ -0,0 +1,26 @@ +/* color palette from */ +@import "tailwindcss"; +@import "flowbite/src/themes/default"; +@plugin "flowbite/plugin"; +@source "../node_modules/flowbite"; +@source "../node_modules/flowbite-datepicker"; + +@theme { + --color-primary-50: #eff6ff; + --color-primary-100: #dbeafe; + --color-primary-200: #bfdbfe; + --color-primary-300: #93c5fd; + --color-primary-400: #60a5fa; + --color-primary-500: #3b82f6; + --color-primary-600: #2563eb; + --color-primary-700: #1d4ed8; + --color-primary-800: #1e40af; + --color-primary-900: #1e3a8a; + + --font-sans: + "Inter", "ui-sans-serif", "system-ui", "-apple-system", "system-ui", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-body: + "Inter", "ui-sans-serif", "system-ui", "-apple-system", "system-ui", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: "ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas", + "Liberation Mono", "Courier New", "monospace"; +} diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg new file mode 100644 index 0000000..7565660 --- /dev/null +++ b/frontend/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css new file mode 100644 index 0000000..8611f02 --- /dev/null +++ b/frontend/src/assets/main.css @@ -0,0 +1 @@ +@import "./base.css"; diff --git a/frontend/src/components/Alert.vue b/frontend/src/components/Alert.vue new file mode 100644 index 0000000..e169427 --- /dev/null +++ b/frontend/src/components/Alert.vue @@ -0,0 +1,15 @@ + + + + diff --git a/frontend/src/components/Breadcrumbs.vue b/frontend/src/components/Breadcrumbs.vue new file mode 100644 index 0000000..7cb9c5e --- /dev/null +++ b/frontend/src/components/Breadcrumbs.vue @@ -0,0 +1,36 @@ + + + diff --git a/frontend/src/components/Dashboard.vue b/frontend/src/components/Dashboard.vue new file mode 100644 index 0000000..0cde7ae --- /dev/null +++ b/frontend/src/components/Dashboard.vue @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/components/DepartmentUpsertModal.vue b/frontend/src/components/DepartmentUpsertModal.vue new file mode 100644 index 0000000..771ced3 --- /dev/null +++ b/frontend/src/components/DepartmentUpsertModal.vue @@ -0,0 +1,113 @@ + + diff --git a/frontend/src/components/Headbar.vue b/frontend/src/components/Headbar.vue new file mode 100644 index 0000000..74dcc70 --- /dev/null +++ b/frontend/src/components/Headbar.vue @@ -0,0 +1,135 @@ + + diff --git a/frontend/src/components/PermissionUpsertModal.vue b/frontend/src/components/PermissionUpsertModal.vue new file mode 100644 index 0000000..8802e9d --- /dev/null +++ b/frontend/src/components/PermissionUpsertModal.vue @@ -0,0 +1,103 @@ + + + diff --git a/frontend/src/components/PopupModal.vue b/frontend/src/components/PopupModal.vue new file mode 100644 index 0000000..a1da435 --- /dev/null +++ b/frontend/src/components/PopupModal.vue @@ -0,0 +1,52 @@ + + + diff --git a/frontend/src/components/PositionUpsertModal.vue b/frontend/src/components/PositionUpsertModal.vue new file mode 100644 index 0000000..5a4941c --- /dev/null +++ b/frontend/src/components/PositionUpsertModal.vue @@ -0,0 +1,102 @@ + + diff --git a/frontend/src/components/RoleUpsertModal.vue b/frontend/src/components/RoleUpsertModal.vue new file mode 100644 index 0000000..8f5dd4a --- /dev/null +++ b/frontend/src/components/RoleUpsertModal.vue @@ -0,0 +1,107 @@ + + + diff --git a/frontend/src/components/SchedulerUpdateModal.vue b/frontend/src/components/SchedulerUpdateModal.vue new file mode 100644 index 0000000..c72c817 --- /dev/null +++ b/frontend/src/components/SchedulerUpdateModal.vue @@ -0,0 +1,88 @@ + + + diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue new file mode 100644 index 0000000..ce42bc2 --- /dev/null +++ b/frontend/src/components/Sidebar.vue @@ -0,0 +1,147 @@ + + + diff --git a/frontend/src/components/TablePagination.vue b/frontend/src/components/TablePagination.vue new file mode 100644 index 0000000..d568e39 --- /dev/null +++ b/frontend/src/components/TablePagination.vue @@ -0,0 +1,78 @@ + + + diff --git a/frontend/src/components/UserUpsertModal.vue b/frontend/src/components/UserUpsertModal.vue new file mode 100644 index 0000000..1feecbb --- /dev/null +++ b/frontend/src/components/UserUpsertModal.vue @@ -0,0 +1,150 @@ + + diff --git a/frontend/src/composables/auth/useUserAuth.ts b/frontend/src/composables/auth/useUserAuth.ts new file mode 100644 index 0000000..93a92c0 --- /dev/null +++ b/frontend/src/composables/auth/useUserAuth.ts @@ -0,0 +1,71 @@ +import client from "@/api/client"; +import { ref } from "vue"; +import useAuthStore from "../store/useAuthStore"; +import useUserStore from "../store/useUserStore"; + +const useUserAuth = () => { + const isAuthenticated = ref(false); + const authStore = useAuthStore(); + const userStore = useUserStore(); + + const queryCurrentUser = async () => { + const { data } = await client.GET("/iam/me"); + return data; + }; + + const refreshCurrentUser = async () => { + const currentUser = await queryCurrentUser(); + if (currentUser) { + userStore.set(currentUser); + isAuthenticated.value = true; + } + }; + + const upsertCurrentUser = async ({ + username, + password, + enable, + }: { + username: string; + password?: string | null; + enable: boolean; + }) => { + await client.POST("/iam/me", { + body: { + username, + password: password ?? undefined, + enable, + }, + }); + await refreshCurrentUser(); + }; + + const signIn = async (username: string, password: string) => { + const signInResponse = await client.POST("/auth/sign-in", { + body: { + username, + password, + }, + }); + authStore.set( + signInResponse.response.headers.get("authorization") ?? undefined, + ); + await refreshCurrentUser(); + }; + + const signOut = () => { + authStore.remove(); + isAuthenticated.value = false; + userStore.remove(); + }; + + return { + isAuthenticated, + signIn, + signOut, + queryCurrentUser, + upsertCurrentUser, + }; +}; + +export default useUserAuth; diff --git a/frontend/src/composables/department/useDepartmentBind.ts b/frontend/src/composables/department/useDepartmentBind.ts new file mode 100644 index 0000000..e4eca25 --- /dev/null +++ b/frontend/src/composables/department/useDepartmentBind.ts @@ -0,0 +1,38 @@ +import client from "@/api/client"; + +export function useDepartmentBind() { + const bindDepartment = async (userId: number, departmentIds: number[]) => { + try { + await client.POST("/iam/department/bind", { + body: { + userId, + departmentIds, + }, + }); + return true; + } catch (error) { + console.error("Error binding departments:", error); + return false; + } + }; + + const unbindDepartment = async (userId: number, departmentIds: number[]) => { + try { + await client.POST("/iam/department/unbind", { + body: { + userId, + departmentIds, + }, + }); + return true; + } catch (error) { + console.error("Error unbinding departments:", error); + return false; + } + }; + + return { + bindDepartment, + unbindDepartment, + }; +} diff --git a/frontend/src/composables/department/useDepartmentDelete.ts b/frontend/src/composables/department/useDepartmentDelete.ts new file mode 100644 index 0000000..7a40b5b --- /dev/null +++ b/frontend/src/composables/department/useDepartmentDelete.ts @@ -0,0 +1,18 @@ +import client from "@/api/client"; + +export const useDepartmentDelete = () => { + const deleteDepartment = async (departmentId: number) => { + await client.DELETE("/department", { + params: { + query: { + id: departmentId, + }, + }, + }); + }; + return { + deleteDepartment, + }; +}; + +export default useDepartmentDelete; diff --git a/frontend/src/composables/department/useDepartmentQuery.ts b/frontend/src/composables/department/useDepartmentQuery.ts new file mode 100644 index 0000000..f8935d7 --- /dev/null +++ b/frontend/src/composables/department/useDepartmentQuery.ts @@ -0,0 +1,45 @@ +import client from "@/api/client"; +import { ref } from "vue"; +import type { components } from "../../api/types/schema"; + +export const useDepartmentQuery = () => { + const total = ref(0); + const departments = ref([]); + const allDepartments = ref([]); + + const fetchAllDepartments = async () => { + const { data } = await client.GET("/department/query"); + allDepartments.value = data ?? []; + }; + const fetchDepartmentWith = async ( + param: { + name?: string; + enable?: boolean; + userId?: number; + bindState?: "ALL" | "BIND" | "UNBIND"; + }, + page = 1, + size = 10, + ) => { + const { data } = await client.GET("/department/page-query", { + params: { + query: { + pageRequestDto: { + page, + size, + }, + departmentQueryDto: param, + }, + }, + }); + total.value = !data || !data.total ? 0 : data.total; + departments.value = data?.data ?? []; + }; + return { + total, + departments, + allDepartments, + fetchDepartmentWith, + fetchAllDepartments, + }; +}; diff --git a/frontend/src/composables/department/useDepartmentUpsert.ts b/frontend/src/composables/department/useDepartmentUpsert.ts new file mode 100644 index 0000000..7dda2be --- /dev/null +++ b/frontend/src/composables/department/useDepartmentUpsert.ts @@ -0,0 +1,18 @@ +import client from "../../api/client"; +import type { DepartmentUpsertModel } from "../../types/department"; + +export const useDepartmentUpsert = () => { + const upsertDepartment = async (department: DepartmentUpsertModel) => { + await client.POST("/department", { + body: { + id: department.id, + name: department.name, + parentId: department.parentId ?? undefined, + }, + }); + }; + + return { + upsertDepartment, + }; +}; diff --git a/frontend/src/composables/job/useJobControl.ts b/frontend/src/composables/job/useJobControl.ts new file mode 100644 index 0000000..74223f6 --- /dev/null +++ b/frontend/src/composables/job/useJobControl.ts @@ -0,0 +1,38 @@ +import client from "@/api/client"; + +export const useJobControl = () => { + const resumeTrigger = async (trigger: { + triggerName: string; + triggerGroup: string; + jobQueryParam?: { + name?: string; + }; + }) => { + await client.POST("/scheduler/trigger/resume", { + body: { + name: trigger.triggerName, + group: trigger.triggerGroup, + }, + }); + }; + + const pauseTrigger = async (trigger: { + triggerName: string; + triggerGroup: string; + jobQueryParam?: { + name?: string; + }; + }) => { + await client.POST("/scheduler/trigger/pause", { + body: { + name: trigger.triggerName, + group: trigger.triggerGroup, + }, + }); + }; + + return { + pauseTrigger, + resumeTrigger, + }; +}; diff --git a/frontend/src/composables/job/useJobQuery.ts b/frontend/src/composables/job/useJobQuery.ts new file mode 100644 index 0000000..eb08543 --- /dev/null +++ b/frontend/src/composables/job/useJobQuery.ts @@ -0,0 +1,36 @@ +import client from "@/api/client"; +import { ref } from "vue"; +import type { components } from "../../api/types/schema"; +export const useJobsPaginationQuery = () => { + const total = ref(0); + const jobs = ref(); + const fetchJobsWith = async ( + queryParam?: { + name?: string; + }, + page = 1, + size = 10, + ) => { + const { data } = await client.GET("/scheduler/page-query", { + params: { + query: { + pageRequestDto: { + page: page, + size: size, + }, + queryDto: { + name: queryParam?.name, + }, + }, + }, + }); + total.value = !data || !data.total ? 0 : data.total; + jobs.value = data?.data ?? []; + }; + + return { + total, + jobs, + fetchJobsWith, + }; +}; diff --git a/frontend/src/composables/job/useJobUpdate.ts b/frontend/src/composables/job/useJobUpdate.ts new file mode 100644 index 0000000..c2dc297 --- /dev/null +++ b/frontend/src/composables/job/useJobUpdate.ts @@ -0,0 +1,24 @@ +import client from "@/api/client"; + +export const useJobUpdate = () => { + const updateCron = async (trigger: { + triggerName: string; + triggerGroup: string; + cron: string; + }) => { + await client.PUT("/scheduler/job/update", { + params: { + query: { + cron: trigger.cron, + }, + }, + body: { + name: trigger.triggerName, + group: trigger.triggerGroup, + }, + }); + }; + return { + updateCron, + }; +}; diff --git a/frontend/src/composables/page.ts b/frontend/src/composables/page.ts new file mode 100644 index 0000000..4af7371 --- /dev/null +++ b/frontend/src/composables/page.ts @@ -0,0 +1,67 @@ +import { computed, ref } from "vue"; + +export interface PaginationState { + currentPage: number; + pageSize: number; + total: number; +} + +export interface UsePaginationOptions { + initialPage?: number; + initialPageSize?: number; + initialTotal?: number; +} + +export function usePagination(options: UsePaginationOptions = {}) { + const { initialPage = 1, initialPageSize = 10, initialTotal = 0 } = options; + + const currentPage = ref(initialPage); + const pageSize = ref(initialPageSize); + const total = ref(initialTotal); + + const totalPages = computed(() => Math.ceil(total.value / pageSize.value)); + + const pageNumbers = computed(() => { + const pages = []; + for (let i = 1; i <= totalPages.value; i++) { + pages.push(i); + } + return pages; + }); + + const displayRange = computed(() => { + const start = + total.value === 0 ? 0 : (currentPage.value - 1) * pageSize.value + 1; + const end = + total.value === 0 + ? 0 + : Math.min(currentPage.value * pageSize.value, total.value); + return { start, end }; + }); + + const isFirstPage = computed( + () => total.value === 0 || currentPage.value === 1, + ); + + const isLastPage = computed( + () => total.value === 0 || currentPage.value === totalPages.value, + ); + + const updatePaginationState = (state: Partial) => { + if (state.currentPage !== undefined) currentPage.value = state.currentPage; + if (state.pageSize !== undefined) pageSize.value = state.pageSize; + if (state.total !== undefined) total.value = state.total; + }; + + return { + currentPage, + pageSize, + total, + totalPages, + pageNumbers, + displayRange, + isFirstPage, + isLastPage, + updatePaginationState, + }; +} diff --git a/frontend/src/composables/permission/usePermissionBind.ts b/frontend/src/composables/permission/usePermissionBind.ts new file mode 100644 index 0000000..5638756 --- /dev/null +++ b/frontend/src/composables/permission/usePermissionBind.ts @@ -0,0 +1,36 @@ +import client from "@/api/client"; +export const usePermissionBind = () => { + const bindPermission = async ({ + roleId, + permissionIds, + }: { + roleId: number; + permissionIds: number[]; + }) => { + await client.POST("/iam/permission/bind", { + body: { + roleId, + permissionIds, + }, + }); + }; + + const unbindPermission = async ({ + roleId, + permissionIds, + }: { + roleId: number; + permissionIds: number[]; + }) => { + await client.POST("/iam/permission/unbind", { + body: { + roleId, + permissionIds, + }, + }); + }; + return { + bindPermission, + unbindPermission, + }; +}; diff --git a/frontend/src/composables/permission/usePermissionDelete.ts b/frontend/src/composables/permission/usePermissionDelete.ts new file mode 100644 index 0000000..40789ff --- /dev/null +++ b/frontend/src/composables/permission/usePermissionDelete.ts @@ -0,0 +1,19 @@ +import client from "@/api/client"; + +const usePermissionDelete = () => { + const deletePermission = async (id: number) => { + await client.DELETE("/iam/permission", { + params: { + query: { + permissionId: id, + }, + }, + }); + }; + + return { + deletePermission, + }; +}; + +export default usePermissionDelete; diff --git a/frontend/src/composables/permission/usePermissionQuery.ts b/frontend/src/composables/permission/usePermissionQuery.ts new file mode 100644 index 0000000..5592157 --- /dev/null +++ b/frontend/src/composables/permission/usePermissionQuery.ts @@ -0,0 +1,48 @@ +import client from "@/api/client"; +import { ref } from "vue"; +import type { components } from "../../api/types/schema"; + +const usePermissionsQuery = () => { + const total = ref(0); + const permissions = ref([]); + const fetchPermissionsWith = async ( + query: { + name?: string; + roleId?: number; + bindState?: "BIND" | "ALL" | "UNBIND"; + }, + page = 1, + size = 10, + ) => { + const { data } = await client.GET("/iam/permissions", { + params: { + query: { + pageRequestDto: { + page, + size, + }, + permissionQueryDto: { + permissionName: query.name, + roleId: query.roleId, + bindState: query.bindState, + }, + }, + }, + }); + + if (!data) { + throw new Error("获取权限数据失败"); + } + + total.value = data.total ?? 0; + permissions.value = data.data ?? []; + }; + + return { + total, + permissions, + fetchPermissionsWith, + }; +}; + +export default usePermissionsQuery; diff --git a/frontend/src/composables/permission/usePermissionUpsert.ts b/frontend/src/composables/permission/usePermissionUpsert.ts new file mode 100644 index 0000000..b101769 --- /dev/null +++ b/frontend/src/composables/permission/usePermissionUpsert.ts @@ -0,0 +1,19 @@ +import client from "../../api/client"; +import type { PermissionUpsertModel } from "../../types/permission"; + +const usePermissionUpsert = () => { + const upsertPermission = async (permission: PermissionUpsertModel) => { + await client.POST("/iam/permission", { + body: { + id: permission.id, + name: permission.name, + code: permission.code, + }, + }); + }; + + return { + upsertPermission, + }; +}; +export default usePermissionUpsert; diff --git a/frontend/src/composables/position/usePositionBind.ts b/frontend/src/composables/position/usePositionBind.ts new file mode 100644 index 0000000..5eb9675 --- /dev/null +++ b/frontend/src/composables/position/usePositionBind.ts @@ -0,0 +1,38 @@ +import client from "@/api/client"; + +export function usePositionBind() { + const bindPosition = async (userId: number, positionIds: number[]) => { + try { + await client.POST("/iam/position/bind", { + body: { + userId, + positionIds, + }, + }); + return true; + } catch (error) { + console.error("Error binding positions:", error); + return false; + } + }; + + const unbindPosition = async (userId: number, positionIds: number[]) => { + try { + await client.POST("/iam/position/unbind", { + body: { + userId, + positionIds, + }, + }); + return true; + } catch (error) { + console.error("Error unbinding positions:", error); + return false; + } + }; + + return { + bindPosition, + unbindPosition, + }; +} diff --git a/frontend/src/composables/position/usePositionDelete.ts b/frontend/src/composables/position/usePositionDelete.ts new file mode 100644 index 0000000..ad65328 --- /dev/null +++ b/frontend/src/composables/position/usePositionDelete.ts @@ -0,0 +1,18 @@ +import client from "@/api/client"; + +export const usePositionDelete = () => { + const deletePosition = async (positionId: number) => { + await client.DELETE("/position", { + params: { + query: { + id: positionId, + }, + }, + }); + }; + return { + deletePosition, + }; +}; + +export default usePositionDelete; diff --git a/frontend/src/composables/position/usePositionQuery.ts b/frontend/src/composables/position/usePositionQuery.ts new file mode 100644 index 0000000..70bdfcf --- /dev/null +++ b/frontend/src/composables/position/usePositionQuery.ts @@ -0,0 +1,45 @@ +import client from "@/api/client"; +import { ref } from "vue"; +import type { components } from "../../api/types/schema"; + +export const usePositionQuery = () => { + const total = ref(0); + const positions = ref([]); + const allPositions = ref([]); + + const fetchAllPositions = async () => { + const { data } = await client.GET("/position/query"); + allPositions.value = data ?? []; + }; + const fetchPositionWith = async ( + param: { + name?: string; + enable?: boolean; + userId?: number; + bindState?: "ALL" | "BIND" | "UNBIND"; + }, + page = 1, + size = 10, + ) => { + const { data } = await client.GET("/position/page-query", { + params: { + query: { + pageRequestDto: { + page, + size, + }, + positionQueryDto: param, + }, + }, + }); + total.value = !data || !data.total ? 0 : data.total; + positions.value = data?.data ?? []; + }; + return { + total, + positions, + allPositions, + fetchPositionWith, + fetchAllPositions, + }; +}; diff --git a/frontend/src/composables/position/usePositionUpsert.ts b/frontend/src/composables/position/usePositionUpsert.ts new file mode 100644 index 0000000..611dc79 --- /dev/null +++ b/frontend/src/composables/position/usePositionUpsert.ts @@ -0,0 +1,16 @@ +import client from "../../api/client"; +import type { components } from "../../api/types/schema"; + +export const usePositionUpsert = () => { + const upsertPosition = async ( + position: components["schemas"]["Position"], + ) => { + await client.POST("/position", { + body: position, + }); + }; + + return { + upsertPosition, + }; +}; diff --git a/frontend/src/composables/role/useRoleBind.ts b/frontend/src/composables/role/useRoleBind.ts new file mode 100644 index 0000000..85e2778 --- /dev/null +++ b/frontend/src/composables/role/useRoleBind.ts @@ -0,0 +1,28 @@ +import client from "@/api/client"; + +export const useRoleBind = () => { + const bindRole = async ({ + userId, + roleIds, + }: { userId: number; roleIds: number[] }) => { + await client.POST("/iam/role/bind", { + body: { + userId, + roleIds, + }, + }); + }; + + const unbindRole = async (userId: number, roleIds: number[]) => { + await client.POST("/iam/role/unbind", { + body: { + userId, + roleIds, + }, + }); + }; + return { + bindRole, + unbindRole, + }; +}; diff --git a/frontend/src/composables/role/useRoleDelete.ts b/frontend/src/composables/role/useRoleDelete.ts new file mode 100644 index 0000000..7edd736 --- /dev/null +++ b/frontend/src/composables/role/useRoleDelete.ts @@ -0,0 +1,19 @@ +import client from "../../api/client"; + +const useRoleDelete = () => { + const deleteRole = async (roleId: number) => { + await client.DELETE("/iam/role", { + params: { + query: { + roleId, + }, + }, + }); + }; + + return { + deleteRole, + }; +}; + +export default useRoleDelete; diff --git a/frontend/src/composables/role/useRoleUpsert.ts b/frontend/src/composables/role/useRoleUpsert.ts new file mode 100644 index 0000000..18961fa --- /dev/null +++ b/frontend/src/composables/role/useRoleUpsert.ts @@ -0,0 +1,17 @@ +import client from "../../api/client"; + +export const useRoleUpsert = () => { + const upsertRole = async (role: { + id?: number; + name: string; + code: string; + }) => { + await client.POST("/iam/role", { + body: role, + }); + }; + + return { + upsertRole, + }; +}; diff --git a/frontend/src/composables/role/useRolesQuery.ts b/frontend/src/composables/role/useRolesQuery.ts new file mode 100644 index 0000000..1bfc8b6 --- /dev/null +++ b/frontend/src/composables/role/useRolesQuery.ts @@ -0,0 +1,61 @@ +import client from "@/api/client"; +import { ref } from "vue"; +import type { components } from "../../api/types/schema"; + +export const useRolesQuery = () => { + const total = ref(0); + const roles = ref(); + const roleWithDetail = ref(); + + const getRoleWithDetail = async (roleId: number) => { + const { data } = await client.GET("/iam/role", { + params: { + query: { + roleId, + }, + }, + }); + roleWithDetail.value = data; + }; + + const fetchRolesWith = async ( + param: { + name?: string; + userId?: number; + bindState?: "BIND" | "ALL" | "UNBIND"; + }, + page = 1, + size = 10, + ) => { + const { data } = await client.GET("/iam/roles", { + params: { + query: { + pageRequestDto: { + page, + size, + }, + roleQueryDto: { + roleName: param.name, + userId: param.userId, + bindState: param.bindState, + }, + }, + }, + }); + + if (!data) { + throw new Error("获取角色数据失败"); + } + + total.value = data.total ?? 0; + roles.value = data.data ?? []; + }; + + return { + total, + roles, + roleWithDetail, + getRoleWithDetail, + fetchRolesWith, + }; +}; diff --git a/frontend/src/composables/store/useAlertStore.ts b/frontend/src/composables/store/useAlertStore.ts new file mode 100644 index 0000000..9bac232 --- /dev/null +++ b/frontend/src/composables/store/useAlertStore.ts @@ -0,0 +1,57 @@ +import { StorageSerializers, useStorage } from "@vueuse/core"; +import { defineStore } from "pinia"; +import { computed } from "vue"; +import type { AlertProps } from "../../types/alert"; + +const useAlertStore = defineStore("alertStore", () => { + const alertStorage = useStorage( + "alert-storage", + { + content: undefined, + level: undefined, + isShow: undefined, + timer: undefined, + }, + localStorage, + { + serializer: StorageSerializers.object, + }, + ); + + const showAlert = ({ + content: newContent, + level: newLevel, + }: { content: string; level: "info" | "success" | "warning" | "error" }) => { + clearTimeout(alertStorage.value.timer); + alertStorage.value = { + content: newContent, + level: newLevel, + isShow: true, + timer: setTimeout(() => { + alertStorage.value.isShow = false; + }, 3000), + }; + }; + + const levelClassName = computed(() => { + if (!alertStorage.value.level) { + return; + } + return { + info: "text-blue-800 bg-blue-50 dark:bg-gray-800 dark:text-blue-400 ", + success: + "text-green-800 bg-green-50 dark:bg-gray-800 dark:text-green-400", + warning: + "text-yellow-800 bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300", + error: "text-red-800 bg-red-50 dark:bg-gray-800 dark:text-red-400", + }[alertStorage.value.level]; + }); + + return { + alertStorage, + showAlert, + levelClassName, + }; +}); + +export default useAlertStore; diff --git a/frontend/src/composables/store/useAuthStore.ts b/frontend/src/composables/store/useAuthStore.ts new file mode 100644 index 0000000..8bde450 --- /dev/null +++ b/frontend/src/composables/store/useAuthStore.ts @@ -0,0 +1,28 @@ +import { StorageSerializers, useStorage } from "@vueuse/core"; +import { defineStore } from "pinia"; + +const useAuthStore = defineStore("authStore", () => { + const tokenStore = useStorage("auth-storage", null, localStorage, { + serializer: StorageSerializers.object, + }); + + const set = (token?: string) => { + tokenStore.value = token; + }; + + const get = () => { + return tokenStore.value; + }; + + const remove = () => { + tokenStore.value = undefined; + }; + + return { + set, + get, + remove, + }; +}); + +export default useAuthStore; diff --git a/frontend/src/composables/store/useUserStore.ts b/frontend/src/composables/store/useUserStore.ts new file mode 100644 index 0000000..4f164cc --- /dev/null +++ b/frontend/src/composables/store/useUserStore.ts @@ -0,0 +1,44 @@ +import { StorageSerializers, useStorage } from "@vueuse/core"; +import { defineStore } from "pinia"; +import { computed } from "vue"; +import type { components } from "../../api/types/schema"; +const useUserStore = defineStore("userStore", () => { + const user = useStorage( + "user-storage", + null, + localStorage, + { + serializer: StorageSerializers.object, + }, + ); + + const set: ( + userRolePermission?: components["schemas"]["UserRolePermissionDto"], + ) => void = (userRolePermission) => { + user.value = userRolePermission; + }; + + function remove() { + user.value = null; + } + + const roleCodes = computed(() => { + return user.value?.roles?.flatMap((role) => role.code); + }); + + const permissionCodes = computed(() => { + return user.value?.roles?.flatMap((role) => + role.permissions?.map((permission) => permission.code), + ); + }); + + return { + user, + set, + remove, + roleCodes, + permissionCodes, + }; +}); + +export default useUserStore; diff --git a/frontend/src/composables/user/useUserDelete.ts b/frontend/src/composables/user/useUserDelete.ts new file mode 100644 index 0000000..55359a2 --- /dev/null +++ b/frontend/src/composables/user/useUserDelete.ts @@ -0,0 +1,19 @@ +import { ref } from "vue"; +import client from "../../api/client"; + +const useUserDelete = () => { + const deleteUser = async (userId: number) => { + await client.DELETE("/iam/user", { + params: { + query: { + userId, + }, + }, + }); + }; + return { + deleteUser, + }; +}; + +export default useUserDelete; diff --git a/frontend/src/composables/user/useUserQuery.ts b/frontend/src/composables/user/useUserQuery.ts new file mode 100644 index 0000000..8286359 --- /dev/null +++ b/frontend/src/composables/user/useUserQuery.ts @@ -0,0 +1,50 @@ +import client from "@/api/client"; +import { ref } from "vue"; +import type { components } from "../../api/types/schema"; + +export const useUserQuery = () => { + const total = ref(0); + const users = ref([]); + const user = ref(); + + const getUserWithDetail = async (userId: number) => { + const { data } = await client.GET("/iam/user", { + params: { + query: { + userId: userId, + }, + }, + }); + user.value = data; + }; + + const fetchUsersWith = async ( + param: { + username?: string; + }, + page = 1, + size = 10, + ) => { + const { data } = await client.GET("/iam/users", { + params: { + query: { + pageRequestDto: { + page, + size, + }, + userQueryDto: param, + }, + }, + }); + total.value = !data || !data.total ? 0 : data.total; + users.value = data?.data ?? []; + }; + + return { + total, + users, + user, + fetchUsersWith, + getUserWithDetail, + }; +}; diff --git a/frontend/src/composables/user/useUserUpsert.ts b/frontend/src/composables/user/useUserUpsert.ts new file mode 100644 index 0000000..0fcc6fc --- /dev/null +++ b/frontend/src/composables/user/useUserUpsert.ts @@ -0,0 +1,20 @@ +import { ref } from "vue"; +import client from "../../api/client"; +import type { UserUpsertSubmitModel } from "../../types/user"; + +export const useUserUpsert = () => { + const upsertUser = async (user: UserUpsertSubmitModel) => { + const { data } = await client.POST("/iam/user", { + body: { + id: user.id, + username: user.username, + password: user.password, + enable: user.enable, + }, + }); + }; + + return { + upsertUser, + }; +}; diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..cdd9efe --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,33 @@ +import "./assets/main.css"; + +import { createPinia } from "pinia"; +import { createApp } from "vue"; + +import App from "./App.vue"; +import useUserAuth from "./composables/auth/useUserAuth"; +import useAlertStore from "./composables/store/useAlertStore"; +import router from "./router"; +import makeErrorHandler from "./utils/errorHandler"; + +async function enableMocking() { + if (import.meta.env.VITE_ENABLE_MOCK === "false") { + return; + } + + const { worker } = await import("./api/mocks/setup"); + + // `worker.start()` returns a Promise that resolves + // once the Service Worker is up and ready to intercept requests. + return worker.start(); +} + +enableMocking().then(() => { + const app = createApp(App); + app.use(createPinia()); + const { signOut } = useUserAuth(); + const { showAlert } = useAlertStore(); + app.use(router); + const errorHandler = makeErrorHandler(router, signOut, showAlert); + app.config.errorHandler = errorHandler; + app.mount("#app"); +}); diff --git a/frontend/src/router/constants.ts b/frontend/src/router/constants.ts new file mode 100644 index 0000000..095a4ea --- /dev/null +++ b/frontend/src/router/constants.ts @@ -0,0 +1,63 @@ +export enum RoutePath { + HOME = "/", + LOGIN = "/login", + DASHBOARD = "/dashboard", + GLOBAL_NOTFOUND = "/:pathMatch(.*)*", + NOTFOUND = ":pathMatch(.*)*", + OVERVIEW = "overview", + USERVIEW = "users", + ROLEVIEW = "roles", + BINDROLEVIEW = "bind-roles/:userId", + BINDPERMISSIONVIEW = "bind-permissions/:roleId", + BINDDEPARTMENTVIEW = "bind-departments/:userId", + BINDPOSITIONVIEW = "bind-positions/:userId", + PERMISSIONVIEW = "permissions", + DEPARTMENTVIEW = "departments", + POSITIONVIEW = "positions", + CREATEUSERVIEW = "create-user", + SCHEDULERVIEW = "scheduler", + UPSERTUSERVIEW = "upsert-user", + UPSERTROLEVIEW = "upsert-role", + UPSERTPERMISSIONVIEW = "upsert-permission", + UPSERTDEPARTMENTVIEW = "upsert-department", + UPSERTPOSITIONVIEW = "upsert-position", + SETTINGS = "settings", +} + +export enum RouteName { + HOME = "home", + LOGIN = "login", + DASHBOARD = "dashboard", + OVERVIEW = "overview", + USERVIEW = "users", + ROLEVIEW = "roles", + BINDROLEVIEW = "bind-roles", + BINDPERMISSIONVIEW = "bind-permissions", + BINDDEPARTMENTVIEW = "bind-departments", + BINDPOSITIONVIEW = "bind-positions", + PERMISSIONVIEW = "permissions", + DEPARTMENTVIEW = "departments", + POSITIONVIEW = "positions", + CREATEUSERVIEW = "create-user", + SCHEDULERVIEW = "scheduler", + UPSERTUSERVIEW = "upsert-user", + UPSERTROLEVIEW = "upsert-role", + UPSERTPERMISSIONVIEW = "upsert-permission", + UPSERTDEPARTMENTVIEW = "upsert-department", + UPSERTPOSITIONVIEW = "upsert-position", + SETTINGS = "settings", + NOTFOUND = "notfound", + GLOBAL_NOTFOUND = "global-notfound", +} + +export enum ROLE { + ADMIN = "ADMIN", + USER = "USER", +} + +export enum PERMISSION { + USER_VIEW = "user:view", + USER_ADD = "user:add", + USER_EDIT = "user:edit", + USER_DELETE = "user:delete", +} diff --git a/frontend/src/router/guards.ts b/frontend/src/router/guards.ts new file mode 100644 index 0000000..596a508 --- /dev/null +++ b/frontend/src/router/guards.ts @@ -0,0 +1,47 @@ +import useUserStore from "@/composables/store/useUserStore"; +import type { NavigationGuard, Router } from "vue-router"; +import type { RouteMeta } from "../types/router"; +import { RoutePath } from "./constants"; + +export const authGuard: NavigationGuard = (to) => { + const userStore = useUserStore(); + if (to.meta.requiresAuth && !userStore.user) { + return { + path: RoutePath.LOGIN, + query: { redirect: to.fullPath }, + }; + } + if (to.path === RoutePath.LOGIN && userStore.user) { + return { path: `${RoutePath.DASHBOARD}/${RoutePath.USERVIEW}` }; + } +}; + +export const permissionGuard: NavigationGuard = (to) => { + const userStore = useUserStore(); + const routeMeta: RouteMeta = to.meta; + if (routeMeta.hasPermission) { + const hasPermission = userStore.permissionCodes?.includes( + routeMeta.hasPermission, + ); + if (!hasPermission) { + return false; + } + } +}; + +export const roleGuard: NavigationGuard = (to) => { + const userStore = useUserStore(); + const routeMeta: RouteMeta = to.meta; + if (routeMeta.hasRole) { + const hasRole = userStore.roleCodes?.includes(routeMeta.hasRole); + if (!hasRole) { + return false; + } + } +}; + +export const setupGuards = (router: Router) => { + router.beforeEach(authGuard); + router.beforeEach(permissionGuard); + router.beforeEach(roleGuard); +}; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..f0f914f --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,29 @@ +import { createRouter, createWebHistory } from "vue-router"; +import type { RouteRecordRaw } from "vue-router"; +import { setupGuards } from "./guards"; + +import authRoutes from "./modules/auth"; +import dashboardRoutes from "./modules/dashboard"; +import errorRoutes from "./modules/error"; +import { RouteName } from "./constants"; + +const routes: RouteRecordRaw[] = [ + dashboardRoutes, + ...authRoutes, + ...errorRoutes, +]; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes, +}); + +router.onError((err) => { + console.error("router err:", err); + // TODO 增加一个错误页面 + router.push(RouteName.USERVIEW); + return false; +}); +setupGuards(router); + +export default router; diff --git a/frontend/src/router/modules/auth.ts b/frontend/src/router/modules/auth.ts new file mode 100644 index 0000000..757ffc4 --- /dev/null +++ b/frontend/src/router/modules/auth.ts @@ -0,0 +1,19 @@ +import type { RouteRecordRaw } from "vue-router"; +import { RouteName, RoutePath } from "../constants"; + +const authRoutes: RouteRecordRaw[] = [ + { + path: RoutePath.HOME, + name: RouteName.HOME, + redirect: { + name: RouteName.LOGIN, + }, + }, + { + path: RoutePath.LOGIN, + name: RouteName.LOGIN, + component: () => import("../../views/LoginView.vue"), + }, +]; + +export default authRoutes; diff --git a/frontend/src/router/modules/dashboard.ts b/frontend/src/router/modules/dashboard.ts new file mode 100644 index 0000000..ae016ae --- /dev/null +++ b/frontend/src/router/modules/dashboard.ts @@ -0,0 +1,67 @@ +import type { RouteRecordRaw } from "vue-router"; +import Dashboard from "../../components/Dashboard.vue"; +import OverView from "../../views/OverView.vue"; +import { ROLE, RouteName, RoutePath } from "../constants"; +import userManagementRoutes from "./user"; + +const dashboardRoutes: RouteRecordRaw = { + path: RoutePath.DASHBOARD, + name: RouteName.DASHBOARD, + component: Dashboard, + meta: { + requiresAuth: true, + }, + children: [ + { + path: RoutePath.OVERVIEW, + name: RouteName.OVERVIEW, + component: () => import("@/views/OverView.vue"), + meta: { + requiresAuth: true, + }, + }, + { + path: RoutePath.SETTINGS, + name: RouteName.SETTINGS, + component: () => import("@/views/SettingsView.vue"), + meta: { + requiresAuth: true, + }, + }, + ...userManagementRoutes, + { + path: RoutePath.NOTFOUND, + name: RouteName.NOTFOUND, + component: () => import("@/views/NotFound.vue"), + }, + { + path: RoutePath.SCHEDULERVIEW, + name: RouteName.SCHEDULERVIEW, + component: () => import("@/views/SchedulerView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + { + path: RoutePath.DEPARTMENTVIEW, + name: RouteName.DEPARTMENTVIEW, + component: () => import("@/views/DepartmentView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + { + path: RoutePath.POSITIONVIEW, + name: RouteName.POSITIONVIEW, + component: () => import("@/views/PositionView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + ], +}; + +export default dashboardRoutes; diff --git a/frontend/src/router/modules/error.ts b/frontend/src/router/modules/error.ts new file mode 100644 index 0000000..2d9b913 --- /dev/null +++ b/frontend/src/router/modules/error.ts @@ -0,0 +1,12 @@ +import type { RouteRecordRaw } from "vue-router"; +import { RouteName, RoutePath } from "../constants"; + +const errorRoutes: RouteRecordRaw[] = [ + { + path: RoutePath.GLOBAL_NOTFOUND, + name: RouteName.GLOBAL_NOTFOUND, + component: () => import("../../views/NotFound.vue"), + }, +]; + +export default errorRoutes; diff --git a/frontend/src/router/modules/user.ts b/frontend/src/router/modules/user.ts new file mode 100644 index 0000000..ce0dca7 --- /dev/null +++ b/frontend/src/router/modules/user.ts @@ -0,0 +1,70 @@ +import type { RouteRecordRaw } from "vue-router"; +import { ROLE, RouteName, RoutePath } from "../constants"; + +const userManagementRoutes: RouteRecordRaw[] = [ + { + path: RoutePath.USERVIEW, + name: RouteName.USERVIEW, + component: () => import("@/views/UserView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + { + path: RoutePath.ROLEVIEW, + name: RouteName.ROLEVIEW, + component: () => import("@/views/RoleView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + { + path: RoutePath.BINDROLEVIEW, + name: RouteName.BINDROLEVIEW, + component: () => import("@/views/BindRoleView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + { + path: RoutePath.BINDDEPARTMENTVIEW, + name: RouteName.BINDDEPARTMENTVIEW, + component: () => import("@/views/BindDepartmentView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + { + path: RoutePath.BINDPERMISSIONVIEW, + name: RouteName.BINDPERMISSIONVIEW, + component: () => import("@/views/BindPermissionView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + { + path: RoutePath.PERMISSIONVIEW, + name: RouteName.PERMISSIONVIEW, + component: () => import("@/views/PermissionView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, + { + path: RoutePath.BINDPOSITIONVIEW, + name: RouteName.BINDPOSITIONVIEW, + component: () => import("@/views/BindPositionView.vue"), + meta: { + requiresAuth: true, + hasRole: ROLE.ADMIN, + }, + }, +]; + +export default userManagementRoutes; diff --git a/frontend/src/types/alert.d.ts b/frontend/src/types/alert.d.ts new file mode 100644 index 0000000..7aceada --- /dev/null +++ b/frontend/src/types/alert.d.ts @@ -0,0 +1,6 @@ +export interface AlertProps { + content?: string; + level?: "info" | "success" | "warning" | "error"; + isShow?: boolean; + timer?: number; +} diff --git a/frontend/src/types/department.d.ts b/frontend/src/types/department.d.ts new file mode 100644 index 0000000..36dedbf --- /dev/null +++ b/frontend/src/types/department.d.ts @@ -0,0 +1,5 @@ +export interface DepartmentUpsertModel { + id?: number; + name: string; + parentId?: number | null; +} diff --git a/frontend/src/types/error.ts b/frontend/src/types/error.ts new file mode 100644 index 0000000..37fa4db --- /dev/null +++ b/frontend/src/types/error.ts @@ -0,0 +1,53 @@ +class HttpError extends Error { + status: number; + detail: string | undefined; + constructor(message: string, status: number, detail: string | undefined) { + super(message); + this.name = "HttpError"; + this.status = status; + this.detail = detail; + } +} + +class UnAuthError extends HttpError { + constructor(status: number, detail?: string) { + super("身份认证异常", status, detail); + this.name = "UnAuthError"; + } +} + +class ForbiddenError extends HttpError { + constructor(status: number, detail?: string) { + super("权限校验异常", status, detail); + this.name = "ForbiddenError"; + } +} + +class SystemError extends HttpError { + constructor(status: number, detail?: string) { + super("系统错误,请稍候再试", status, detail); + this.name = "SystemError"; + } +} + +class InternalServerError extends HttpError { + constructor(status: number, detail?: string) { + super("服务器错误,请稍候再试", status, detail); + this.name = "InternalServerError"; + } +} + +class BadRequestError extends HttpError { + constructor(status: number, detail?: string) { + super("请求非法", status, detail); + this.name = "BadRequestError"; + } +} + +export { + UnAuthError, + ForbiddenError, + SystemError, + InternalServerError, + BadRequestError, +}; diff --git a/frontend/src/types/permission.d.ts b/frontend/src/types/permission.d.ts new file mode 100644 index 0000000..d43584e --- /dev/null +++ b/frontend/src/types/permission.d.ts @@ -0,0 +1,5 @@ +export interface PermissionUpsertModel { + id?: number; + name: string; + code: string; +} diff --git a/frontend/src/types/position.d.ts b/frontend/src/types/position.d.ts new file mode 100644 index 0000000..0e858fb --- /dev/null +++ b/frontend/src/types/position.d.ts @@ -0,0 +1,4 @@ +export interface PositionUpsertModel { + id?: number; + name: string; +} diff --git a/frontend/src/types/role.d.ts b/frontend/src/types/role.d.ts new file mode 100644 index 0000000..3c70923 --- /dev/null +++ b/frontend/src/types/role.d.ts @@ -0,0 +1,5 @@ +export interface RoleUpsertModel { + id?: number; + name: string; + code: string; +} diff --git a/frontend/src/types/router.d.ts b/frontend/src/types/router.d.ts new file mode 100644 index 0000000..6e3b3e0 --- /dev/null +++ b/frontend/src/types/router.d.ts @@ -0,0 +1,11 @@ +import "vue-router"; + +declare module "vue-router" { + interface RouteMeta { + requiresAuth?: boolean; + hasPermission?: string; + hasRole?: string; + } +} + +export { RouteMeta }; diff --git a/frontend/src/types/user.d.ts b/frontend/src/types/user.d.ts new file mode 100644 index 0000000..51ae374 --- /dev/null +++ b/frontend/src/types/user.d.ts @@ -0,0 +1,8 @@ +export interface UserUpsertSubmitModel { + id?: number; + username: string; + password?: string; + enable: boolean; +} + +export type User = UserRolePermissionModel | null; diff --git a/frontend/src/utils/errorHandler.ts b/frontend/src/utils/errorHandler.ts new file mode 100644 index 0000000..fc9c6ce --- /dev/null +++ b/frontend/src/utils/errorHandler.ts @@ -0,0 +1,50 @@ +import type { ComponentPublicInstance } from "vue"; +import type { Router } from "vue-router"; +import { RoutePath } from "../router/constants"; +import { + ForbiddenError, + InternalServerError, + SystemError, + UnAuthError, +} from "../types/error"; + +const makeErrorHandler = + ( + router: Router, + signOut: () => void, + showAlert: ({ + content, + level, + }: { + content: string; + level: "info" | "success" | "warning" | "error"; + }) => void, + ) => + (err: unknown, instance: ComponentPublicInstance | null, info: string) => { + console.error(err); + if (err instanceof UnAuthError) { + signOut(); + router.push(RoutePath.LOGIN); + showAlert({ + level: "error", + content: err.message, + }); + } else if (err instanceof ForbiddenError) { + showAlert({ + level: "error", + content: err.message, + }); + } else if (err instanceof SystemError) { + showAlert({ + level: "error", + content: err.message, + }); + } else if (err instanceof InternalServerError) { + showAlert({ + level: "error", + content: err.detail ?? err.message, + }); + } + }; + +export default makeErrorHandler; diff --git a/frontend/src/views/BindDepartmentView.vue b/frontend/src/views/BindDepartmentView.vue new file mode 100644 index 0000000..a77a486 --- /dev/null +++ b/frontend/src/views/BindDepartmentView.vue @@ -0,0 +1,242 @@ + + + + + diff --git a/frontend/src/views/BindPermissionView.vue b/frontend/src/views/BindPermissionView.vue new file mode 100644 index 0000000..47d4a5d --- /dev/null +++ b/frontend/src/views/BindPermissionView.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/frontend/src/views/BindPositionView.vue b/frontend/src/views/BindPositionView.vue new file mode 100644 index 0000000..ada86b9 --- /dev/null +++ b/frontend/src/views/BindPositionView.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/frontend/src/views/BindRoleView.vue b/frontend/src/views/BindRoleView.vue new file mode 100644 index 0000000..3b51fe3 --- /dev/null +++ b/frontend/src/views/BindRoleView.vue @@ -0,0 +1,235 @@ + + + + + diff --git a/frontend/src/views/DepartmentView.vue b/frontend/src/views/DepartmentView.vue new file mode 100644 index 0000000..dbd628b --- /dev/null +++ b/frontend/src/views/DepartmentView.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue new file mode 100644 index 0000000..907c620 --- /dev/null +++ b/frontend/src/views/LoginView.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/frontend/src/views/NotFound.vue b/frontend/src/views/NotFound.vue new file mode 100644 index 0000000..daf476f --- /dev/null +++ b/frontend/src/views/NotFound.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/views/OverView.vue b/frontend/src/views/OverView.vue new file mode 100644 index 0000000..0ffcb43 --- /dev/null +++ b/frontend/src/views/OverView.vue @@ -0,0 +1,584 @@ + + + + + diff --git a/frontend/src/views/PermissionView.vue b/frontend/src/views/PermissionView.vue new file mode 100644 index 0000000..906d903 --- /dev/null +++ b/frontend/src/views/PermissionView.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/frontend/src/views/PositionView.vue b/frontend/src/views/PositionView.vue new file mode 100644 index 0000000..543be5b --- /dev/null +++ b/frontend/src/views/PositionView.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/frontend/src/views/RoleView.vue b/frontend/src/views/RoleView.vue new file mode 100644 index 0000000..1250911 --- /dev/null +++ b/frontend/src/views/RoleView.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/frontend/src/views/SchedulerView.vue b/frontend/src/views/SchedulerView.vue new file mode 100644 index 0000000..d37b5ff --- /dev/null +++ b/frontend/src/views/SchedulerView.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue new file mode 100644 index 0000000..a3fae3b --- /dev/null +++ b/frontend/src/views/SettingsView.vue @@ -0,0 +1,154 @@ + + + diff --git a/frontend/src/views/UserView.vue b/frontend/src/views/UserView.vue new file mode 100644 index 0000000..041b9eb --- /dev/null +++ b/frontend/src/views/UserView.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..41adfd2 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "allowJs": true, + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..dc744cc --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..dc6fc7b --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "module": "ESNext", + "moduleResolution": "Bundler", + "noUncheckedIndexedAccess": true, + "types": ["node"] + } +} diff --git a/frontend/tsconfig.vitest.json b/frontend/tsconfig.vitest.json new file mode 100644 index 0000000..808658b --- /dev/null +++ b/frontend/tsconfig.vitest.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.app.json", + "include": ["src/**/__tests__/*", "env.d.ts"], + "exclude": [], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", + "lib": [], + "types": ["node", "@vitest/browser/providers/playwright"] + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..eecea0e --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,29 @@ +import { URL, fileURLToPath } from "node:url"; + +import tailwindcss from "@tailwindcss/vite"; +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; +import { loadEnv } from "vite"; +import vueDevTools from "vite-plugin-vue-devtools"; + +// https://vite.dev/config/ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + return { + server: { + port: Number(env.VITE_APP_PORT), + }, + preview: { + port: Number(env.VITE_APP_PORT), + }, + plugins: [vue(), vueDevTools(), tailwindcss()], + build: { + sourcemap: Boolean(env.VITE_SOURCE_MAP), + }, + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, + }; +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..35adc05 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,26 @@ +import { fileURLToPath } from "node:url"; +import { configDefaults, defineConfig, mergeConfig } from "vitest/config"; +import viteConfig from "./vite.config"; + +export default defineConfig(({ command, mode }) => { + return mergeConfig( + { + test: { + inspectBrk: false, + fileParallelism: true, + testTimeout: 0, + exclude: [...configDefaults.exclude, "e2e/**"], + root: fileURLToPath(new URL("./", import.meta.url)), + browser: { + enabled: true, + headless: false, + provider: "playwright", + // https://vitest.dev/guide/browser/playwright + instances: [{ browser: "chromium" }], + screenshotFailures: false, + }, + }, + }, + viteConfig({ mode, command }), + ); +});