diff --git a/.air.toml b/.air.toml new file mode 100644 index 00000000..1352f5ce --- /dev/null +++ b/.air.toml @@ -0,0 +1,69 @@ +# Config file for [Air](https://github.com/air-verse/air) in TOML format + +# Working directory +# . or absolute path, please note that the directories following must be under root. +root = "." +tmp_dir = "storage/temp" + +[build] +# Array of commands to run before each build +pre_cmd = [] +# Just plain old shell command. You could use `make` as well. +cmd = "go build -o storage/temp/main.exe ./cmd/app" +# Array of commands to run after ^C +post_cmd = [] +# Binary file yields from `cmd`. +bin = "storage/temp/main.exe" +# Customize binary, can setup environment variables when run your app. +full_bin = "" +# Watch these filename extensions. +include_ext = ["go", "tpl", "tmpl", "html"] +# Ignore these filename extensions or directories. +exclude_dir = ["storage", "web"] +# Watch these directories if you specified. +include_dir = [] +# Watch these files. +include_file = [] +# Exclude files. +exclude_file = [] +# Exclude specific regular expressions. +exclude_regex = ["_test\\.go"] +# Exclude unchanged files. +exclude_unchanged = true +# Follow symlink for directories +follow_symlink = true +# This log file places in your tmp_dir. +log = "build-errors.log" +# It's not necessary to trigger build each time file changes if it's too frequent. +delay = 2000 +# Stop running old binary when build errors occur. +stop_on_error = true +# Send Interrupt signal before killing process (windows does not support this feature) +send_interrupt = false +# Delay after sending Interrupt signal +kill_delay = 500 # nanosecond +# Rerun binary or not +rerun = false +# Delay after each execution +rerun_delay = 500 + +[log] +# Show log time +time = false +# Only show main log (silences watcher, build, runner) +main_only = false + +[color] +# Customize each part's color. If no color found, use the raw app log. +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +# Delete tmp directory on exit +clean_on_exit = true + +[screen] +clear_on_rebuild = true +keep_scroll = true \ No newline at end of file diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 00000000..ae0fc941 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,55 @@ +name: Backend +on: + push: + branches: + - main + pull_request: +jobs: + build: + runs-on: ubuntu-24.04 + strategy: + matrix: + goarch: [ amd64, arm64 ] + fail-fast: true + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + cache: true + go-version: 'stable' + - name: Install dependencies + run: go mod tidy + - name: Wait for frontend build + uses: lewagon/wait-on-check-action@v1.3.4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + check-name: 'build (frontend)' + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Download frontend + uses: dawidd6/action-download-artifact@v6 + with: + workflow: build-frontend.yml + name: frontend + path: internal/embed/frontend + check_artifacts: true + - name: Build ${{ matrix.goarch }} + env: + CGO_ENABLED: 0 + GOOS: linux + GOARCH: ${{ matrix.goarch }} + run: | + go build -ldflags '-s -w --extldflags "-static"' -o panel-${{ matrix.goarch }} ./cmd/app + go build -ldflags '-s -w --extldflags "-static"' -o cli-${{ matrix.goarch }} ./cmd/cli + - name: Compress ${{ matrix.goarch }} + run: | + upx --best --lzma panel-${{ matrix.goarch }} + upx --best --lzma cli-${{ matrix.goarch }} + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: panel-${{ matrix.goarch }} + path: | + panel-${{ matrix.goarch }} + cli-${{ matrix.goarch }} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index dfb345b8..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Build -on: - push: - branches: - - main - pull_request: -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - goarch: [ amd64, arm64 ] - fail-fast: true - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - cache: true - go-version: '1.22' - - name: Install dependencies - run: go mod tidy - - name: Build ${{ matrix.goarch }} - env: - CGO_ENABLED: 0 - GOOS: linux - GOARCH: ${{ matrix.goarch }} - run: go build -ldflags '-s -w --extldflags "-static"' -tags='nomsgpack' -o panel-${{ matrix.goarch }} - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: panel-${{ matrix.goarch }} - path: panel-${{ matrix.goarch }} diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index a4d8d00b..693bed8f 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -6,12 +6,15 @@ on: pull_request: jobs: codecov: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: - go-version: '1.22' + cache: true + go-version: 'stable' - name: Install dependencies run: go mod tidy - name: Run tests with coverage @@ -20,4 +23,4 @@ jobs: uses: codecov/codecov-action@v4 with: file: ./coverage.out - token: ${{ secrets.CODECOV }} + token: ${{ secrets.CODECOV }} \ No newline at end of file diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..65ddb6fd --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,42 @@ +name: Frontend +on: + push: + branches: + - main + pull_request: +jobs: + build: + name: build (frontend) + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: web + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + run_install: true + package_json_file: web/package.json + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: web/pnpm-lock.yaml + - name: Build frontend + # We need to run the dev server first to generate the auto-imports files + run: | + cp .env.production .env + cp settings/proxy-config.ts.example settings/proxy-config.ts + pnpm dev & + sleep 5 + kill %1 + pnpm build + - name: Upload frontend + uses: actions/upload-artifact@v4 + with: + name: frontend + path: web/dist/ # https://github.com/actions/upload-artifact/issues/541 \ No newline at end of file diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 48437327..198e876c 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -7,25 +7,34 @@ permissions: contents: write jobs: goreleaser: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Go + - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.22' - - name: Fetch Frontend - run: | - curl -sSL https://api.github.com/repos/TheTNB/panel-frontend/releases/latest | jq -r ".assets[] | select(.name | contains(\"dist\")) | .browser_download_url" | xargs curl -L -o frontend.zip - unzip frontend.zip - mv dist/* embed/frontend/ + cache: true + go-version: 'stable' + - name: Wait for frontend build + uses: lewagon/wait-on-check-action@v1.3.4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + check-name: 'build (frontend)' + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Download frontend + uses: dawidd6/action-download-artifact@v6 + with: + workflow: build-frontend.yml + name: frontend + path: internal/embed/frontend + check_artifacts: true - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --clean env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/issue-auto-reply.yml b/.github/workflows/issue-auto-reply.yml index 0ef9c37a..01b9346c 100644 --- a/.github/workflows/issue-auto-reply.yml +++ b/.github/workflows/issue-auto-reply.yml @@ -1,18 +1,15 @@ name: Issue Auto Reply - on: issues: types: [ labeled ] - permissions: contents: read - jobs: issue-reply: permissions: issues: write pull-requests: write - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: ✏️ Feature if: github.event.label.name == '✏️ Feature' @@ -27,7 +24,7 @@ jobs: 我们认为您的建议非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献! We think your suggestion is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution! - ![aoligei](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0) + ![干](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0) - name: ☢️ Bug if: github.event.label.name == '☢️ Bug' uses: actions-cool/issues-helper@v3 @@ -41,4 +38,4 @@ jobs: 我们认为您的反馈非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献! We think your feedback is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution! - ![aoligei](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0) + ![干](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0) \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 30323898..1d04df72 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,18 +7,73 @@ on: permissions: contents: read jobs: - lint: - name: lint - runs-on: ubuntu-latest + golangci: + name: golanci-lint + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: - go-version: '1.22' - cache: false - - name: Lint + cache: true + go-version: 'stable' + - name: Run golangci-lint uses: golangci/golangci-lint-action@v6 with: skip-cache: true version: latest args: --timeout=30m ./... + nilaway: + runs-on: ubuntu-24.04 + if: false + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + cache: true + go-version: 'stable' + - name: Install dependencies + run: go mod tidy + - name: Install NilAway + run: go install go.uber.org/nilaway/cmd/nilaway@latest + - name: Run NilAway + run: nilaway -include-pkgs="github.com/TheTNB/panel" ./... + govulncheck: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + cache: true + go-version: 'stable' + - name: Install Govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + - name: Run Govulncheck + run: govulncheck ./... + frontend: + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: web + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: latest + run_install: true + package_json_file: web/package.json + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: web/pnpm-lock.yaml + - name: Run pnpm lint + run: pnpm lint \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 744b8ad4..84abeb42 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,19 +5,20 @@ on: - main pull_request: jobs: - test: - runs-on: ubuntu-latest + unit: + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 with: - go-version: '1.22' + cache: true + go-version: 'stable' - name: Install dependencies run: sudo apt-get install -y curl jq - name: Set up environment run: | - cp panel-example.conf .env - echo "DB_FILE=$(pwd)/storage/panel.db" >> .env - go run . artisan key:generate + cp config/config.example.yml config/config.yml - name: Run tests - run: go test ./... + run: go test ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 32b14049..243b118a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,6 @@ -tmp -/.air.toml -/panel.conf - -# Golang # # `go test -c` 生成的二进制文件 *.test + # go coverage 工具 *.out *.prof @@ -14,16 +10,16 @@ _cgo_defun.c _cgo_gotypes.go _cgo_export.* -# 编译文件 # +# 编译文件 *.com *.class *.dll *.exe *.o *.so -/panel +# 在此添加你的项目名(如果需要) -# 压缩包 # +# 压缩包 # Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit *.7z *.dmg @@ -34,16 +30,17 @@ _cgo_export.* *.tar *.zip -# 日志文件和数据库 # +# 日志文件和数据库及配置 *.log *.sqlite *.db +config/config.yml -# 临时文件 # +# 临时文件 tmp/ .tmp/ -# 系统生成文件 # +# 系统生成文件 .DS_Store .DS_Store? .AppleDouble @@ -58,7 +55,7 @@ Thumbs.db .VolumeIcon.icns .com.apple.timemachine.donotpresent -# IDE 和编辑器 # +# IDE 和编辑器 .idea/ /go_build_* out/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 6e3a813e..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,90 +0,0 @@ -image: golang:1.23-bookworm - -# 在每个任务执行前运行 -before_script: - - mkdir -p .go - - go version - - go env -w GO111MODULE=on - - go env -w GOPROXY=https://goproxy.cn,direct - -.go_cache: - variables: - GOPATH: $CI_PROJECT_DIR/.go - cache: - paths: - - .go/pkg/mod/ - -stages: - - prepare - - build - - release - -golangci_lint: - stage: prepare - image: golangci/golangci-lint:latest-alpine - extends: .go_cache - allow_failure: true - script: - - golangci-lint run --timeout 30m - -unit_test: - stage: prepare - extends: .go_cache - allow_failure: true - script: - - rm -rf /etc/apt/sources.list - - rm -rf /etc/apt/sources.list.d/* - - wget -O /etc/apt/sources.list https://mirrors.ustc.edu.cn/repogen/conf/debian-http-4-bookworm - - apt-get update - - apt-get install -y curl jq - - cp panel-example.conf .env - - echo "DB_FILE=$(pwd)/storage/panel.db" >> .env - - go run . artisan key:generate - - go test -v -coverprofile=coverage.txt -covermode=atomic ./... - -build: - stage: build - extends: .go_cache - script: - - go mod download - - CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static"' -tags='nomsgpack' -o panel - artifacts: - name: "panel" - paths: - - panel - expire_in: 3 days - -fetch: - stage: build - image: alpine:latest - before_script: - - sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories - - apk add --no-cache curl jq unzip zip - script: - - curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip - - unzip frontend.zip - - mv dist/* embed/frontend/ - artifacts: - name: "frontend" - paths: - - embed/frontend - expire_in: 3 days - -release: - stage: release - dependencies: - - build - - fetch - image: - name: goreleaser/goreleaser - entrypoint: [ '' ] - only: - - tags - variables: - # Disable shallow cloning so that goreleaser can diff between tags to - # generate a changelog. - GIT_DEPTH: 0 - script: - - sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories - - apk add --no-cache upx - - goreleaser release --clean diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 047655f1..79c157cf 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -2,21 +2,29 @@ project_name: panel builds: - id: panel + main: ./cmd/app binary: panel env: - CGO_ENABLED=0 - - GOPROXY=https://goproxy.cn,direct goos: - linux goarch: - amd64 - arm64 - goamd64: - - v2 ldflags: - -s -w --extldflags "-static" - tags: - - nomsgpack + - id: cli + main: ./cmd/cli + binary: cli + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + ldflags: + - -s -w --extldflags "-static" upx: - enabled: true diff --git a/README.md b/README.md index 127ed8c5..4a3bc88d 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ ## 项目现状 -**目前我在着手使用新的「自研」框架重构本项目,由于更改非常大需要一定时间,预期 9 月初会带来新的更新。** +**目前我在着手使用新的「自研」框架重构本项目,由于更改非常大需要一定时间,预期 9 月底会带来新的更新。** ## 优势 diff --git a/app/console/commands/cert_renew.go b/app/console/commands/cert_renew.go deleted file mode 100644 index a8fce5ee..00000000 --- a/app/console/commands/cert_renew.go +++ /dev/null @@ -1,78 +0,0 @@ -package commands - -import ( - "context" - - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal/services" - panelcert "github.com/TheTNB/panel/v2/pkg/cert" - "github.com/TheTNB/panel/v2/pkg/types" -) - -// CertRenew 证书续签 -type CertRenew struct { -} - -// Signature The name and signature of the console command. -func (receiver *CertRenew) Signature() string { - return "panel:cert-renew" -} - -// Description The console command description. -func (receiver *CertRenew) Description() string { - ctx := context.Background() - return facades.Lang(ctx).Get("commands.panel:cert-renew.description") -} - -// Extend The console command extend. -func (receiver *CertRenew) Extend() command.Extend { - return command.Extend{ - Category: "panel", - } -} - -// Handle Execute the console command. -func (receiver *CertRenew) Handle(console.Context) error { - if types.Status != types.StatusNormal { - return nil - } - - var certs []models.Cert - err := facades.Orm().Query().With("Website").With("User").With("DNS").Find(&certs) - if err != nil { - return err - } - - for _, cert := range certs { - if !cert.AutoRenew { - continue - } - - decode, err := panelcert.ParseCert(cert.Cert) - if err != nil { - continue - } - - // 结束时间大于 7 天的证书不续签 - endTime := carbon.FromStdTime(decode.NotAfter) - if endTime.Gt(carbon.Now().AddDays(7)) { - continue - } - - certService := services.NewCertImpl() - _, err = certService.Renew(cert.ID) - if err != nil { - facades.Log().Tags("面板", "证书管理").With(map[string]any{ - "cert_id": cert.ID, - "error": err.Error(), - }).Infof("证书续签失败") - } - } - - return nil -} diff --git a/app/console/commands/monitoring.go b/app/console/commands/monitoring.go deleted file mode 100644 index 59a24494..00000000 --- a/app/console/commands/monitoring.go +++ /dev/null @@ -1,91 +0,0 @@ -package commands - -import ( - "context" - - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/color" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/tools" - "github.com/TheTNB/panel/v2/pkg/types" -) - -// Monitoring 系统监控 -type Monitoring struct { -} - -// Signature The name and signature of the console command. -func (receiver *Monitoring) Signature() string { - return "panel:monitoring" -} - -// Description The console command description. -func (receiver *Monitoring) Description() string { - ctx := context.Background() - return facades.Lang(ctx).Get("commands.panel:monitoring.description") -} - -// Extend The console command extend. -func (receiver *Monitoring) Extend() command.Extend { - return command.Extend{ - Category: "panel", - } -} - -// Handle Execute the console command. -func (receiver *Monitoring) Handle(console.Context) error { - if types.Status != types.StatusNormal { - return nil - } - - // 将等待中的任务分发 - task := services.NewTaskImpl() - _ = task.DispatchWaiting() - - setting := services.NewSettingImpl() - monitor := setting.Get(models.SettingKeyMonitor) - if !cast.ToBool(monitor) { - return nil - } - - info := tools.GetMonitoringInfo() - translate := facades.Lang(context.Background()) - - // 去除部分数据以减少数据库存储 - info.Disk = nil - info.Cpus = nil - - if types.Status != types.StatusNormal { - return nil - } - err := facades.Orm().Query().Create(&models.Monitor{ - Info: info, - }) - if err != nil { - facades.Log().Tags("面板", "系统监控").With(map[string]any{ - "error": err.Error(), - }).Infof("保存失败") - color.Red().Printfln(translate.Get("commands.panel:monitoring.fail")+": %s", err.Error()) - return nil - } - - // 删除过期数据 - days := cast.ToInt(setting.Get(models.SettingKeyMonitorDays)) - if days <= 0 || types.Status != types.StatusNormal { - return nil - } - if _, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{}); err != nil { - facades.Log().Tags("面板", "系统监控").With(map[string]any{ - "error": err.Error(), - }).Infof("删除过期数据失败") - return nil - } - - return nil -} diff --git a/app/console/commands/panel.go b/app/console/commands/panel.go deleted file mode 100644 index 0acb6c40..00000000 --- a/app/console/commands/panel.go +++ /dev/null @@ -1,732 +0,0 @@ -package commands - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/color" - "github.com/spf13/cast" - - requests "github.com/TheTNB/panel/v2/app/http/requests/website" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/tools" - "github.com/TheTNB/panel/v2/pkg/types" -) - -// Panel 面板命令行 -type Panel struct { -} - -// Signature The name and signature of the console command. -func (receiver *Panel) Signature() string { - return "panel" -} - -// Description The console command description. -func (receiver *Panel) Description() string { - ctx := context.Background() - return facades.Lang(ctx).Get("commands.panel.description") -} - -// Extend The console command extend. -func (receiver *Panel) Extend() command.Extend { - return command.Extend{ - Category: "panel", - } -} - -// Handle Execute the console command. -func (receiver *Panel) Handle(ctx console.Context) error { - action := ctx.Argument(0) - arg1 := ctx.Argument(1) - arg2 := ctx.Argument(2) - arg3 := ctx.Argument(3) - arg4 := ctx.Argument(4) - arg5 := ctx.Argument(5) - - translate := facades.Lang(context.Background()) - - switch action { - case "init": - var check models.User - err := facades.Orm().Query().FirstOrFail(&check) - if err == nil { - color.Red().Printfln(translate.Get("commands.panel.init.exist")) - return nil - } - - settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}} - err = facades.Orm().Query().Create(&settings) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.init.fail")) - return nil - } - - hash, err := facades.Hash().Make(str.RandomString(32)) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.init.fail")) - return nil - } - - user := services.NewUserImpl() - _, err = user.Create("admin", hash) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.init.adminFail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.init.success")) - - case "update": - var task models.Task - if err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task); err == nil { - color.Red().Printfln(translate.Get("commands.panel.update.taskCheck")) - return nil - } - if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil { - types.Status = types.StatusFailed - color.Red().Printfln(translate.Get("commands.panel.update.dbFail")) - return nil - } - - panel, err := tools.GetLatestPanelVersion() - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.update.versionFail")) - return err - } - - // 停止面板服务,因为在shell中运行的和systemd的不同 - _ = systemctl.Stop("panel") - - types.Status = types.StatusUpgrade - if err = tools.UpdatePanel(panel); err != nil { - types.Status = types.StatusFailed - color.Red().Printfln(translate.Get("commands.panel.update.fail") + ": " + err.Error()) - return nil - } - - types.Status = types.StatusNormal - color.Green().Printfln(translate.Get("commands.panel.update.success")) - tools.RestartPanel() - - case "getInfo": - var user models.User - err := facades.Orm().Query().Where("id", 1).FirstOrFail(&user) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.getInfo.adminGetFail")) - return nil - } - - password := str.RandomString(16) - hash, err := facades.Hash().Make(password) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.getInfo.passwordGenerationFail")) - return nil - } - user.Username = str.RandomString(8) - user.Password = hash - if user.Email == "" { - user.Email = str.RandomString(8) + "@example.com" - } - - err = facades.Orm().Query().Save(&user) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.getInfo.adminSaveFail")) - return nil - } - - port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.portFail")) - return nil - } - ip, err := tools.GetPublicIP() - if err != nil { - ip = "127.0.0.1" - } - protocol := "http" - if facades.Config().GetBool("panel.ssl") { - protocol = "https" - } - - color.Green().Printfln(translate.Get("commands.panel.getInfo.username") + ": " + user.Username) - color.Green().Printfln(translate.Get("commands.panel.getInfo.password") + ": " + password) - color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port) - color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("panel.entrance")) - color.Green().Printfln(translate.Get("commands.panel.getInfo.address") + ": " + protocol + "://" + ip + ":" + port + facades.Config().GetString("panel.entrance")) - - case "getPort": - port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.portFail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port) - - case "getEntrance": - color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("panel.entrance")) - - case "deleteEntrance": - oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.deleteEntrance.fail")) - return nil - } - if _, err = shell.Execf("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=/!g' /www/panel/panel.conf"); err != nil { - color.Red().Printfln(translate.Get("commands.panel.deleteEntrance.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.deleteEntrance.success")) - - case "writePlugin": - slug := arg1 - version := arg2 - if len(slug) == 0 || len(version) == 0 { - color.Red().Printfln(translate.Get("commands.panel.writePlugin.paramFail")) - return nil - } - - var plugin models.Plugin - err := facades.Orm().Query().UpdateOrCreate(&plugin, models.Plugin{ - Slug: slug, - }, models.Plugin{ - Version: version, - }) - - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.writePlugin.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.writePlugin.success")) - - case "deletePlugin": - slug := arg1 - if len(slug) == 0 { - color.Red().Printfln(translate.Get("commands.panel.deletePlugin.paramFail")) - return nil - } - - _, err := facades.Orm().Query().Where("slug", slug).Delete(&models.Plugin{}) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.deletePlugin.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.deletePlugin.success")) - - case "writeMysqlPassword": - password := arg1 - if len(password) == 0 { - color.Red().Printfln(translate.Get("commands.panel.writeMysqlPassword.paramFail")) - return nil - } - - var setting models.Setting - err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{ - Key: models.SettingKeyMysqlRootPassword, - }, models.Setting{ - Value: password, - }) - - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.writeMysqlPassword.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.writeMysqlPassword.success")) - - case "cleanTask": - _, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).Update("status", models.TaskStatusFailed) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.cleanTask.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.cleanTask.success")) - - case "backup": - backupType := arg1 - name := arg2 - path := arg3 - save := arg4 - hr := `+----------------------------------------------------` - if len(backupType) == 0 || len(name) == 0 || len(path) == 0 || len(save) == 0 { - color.Red().Printfln(translate.Get("commands.panel.backup.paramFail")) - return nil - } - - color.Green().Printfln(hr) - color.Green().Printfln("★ " + translate.Get("commands.panel.backup.start") + " [" + carbon.Now().ToDateTimeString() + "]") - color.Green().Printfln(hr) - - if !io.Exists(path) { - if err := io.Mkdir(path, 0644); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.backupDirFail") + ": " + err.Error()) - return nil - } - } - - switch backupType { - case "website": - color.Yellow().Printfln("|-" + translate.Get("commands.panel.backup.targetSite") + ": " + name) - var website models.Website - if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.siteNotExist")) - color.Green().Printfln(hr) - return nil - } - - backupFile := path + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip" - if _, err := shell.Execf(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.backupFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.backupSuccess")) - - case "mysql": - rootPassword := services.NewSettingImpl().Get(models.SettingKeyMysqlRootPassword) - backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql" - - err := os.Setenv("MYSQL_PWD", rootPassword) - if err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.mysqlBackupFail") + ": " + err.Error()) - color.Green().Printfln(hr) - return nil - } - - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.targetMysql") + ": " + name) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startExport")) - if _, err = shell.Execf(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.exportSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startCompress")) - if _, err = shell.Execf("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error()) - return nil - } - if err := io.Remove("/tmp/" + backupFile); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.compressSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startMove")) - if err := io.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.moveSuccess")) - _ = os.Unsetenv("MYSQL_PWD") - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.success")) - - case "postgresql": - backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql" - check, err := shell.Execf(`su - postgres -c "psql -l" 2>&1`) - if err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.databaseGetFail") + ": " + err.Error()) - color.Green().Printfln(hr) - return nil - } - if !strings.Contains(check, name) { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.databaseNotExist")) - color.Green().Printfln(hr) - return nil - } - - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.targetPostgres") + ": " + name) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startExport")) - if _, err = shell.Execf(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.exportSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startCompress")) - if _, err = shell.Execf("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error()) - return nil - } - if err := io.Remove("/tmp/" + backupFile); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.compressSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startMove")) - if err := io.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.moveSuccess")) - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.success")) - } - - color.Green().Printfln(hr) - files, err := os.ReadDir(path) - if err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error()) - return nil - } - var filteredFiles []os.FileInfo - for _, file := range files { - if strings.HasPrefix(file.Name(), name) && strings.HasSuffix(file.Name(), ".zip") { - fileInfo, err := os.Stat(filepath.Join(path, file.Name())) - if err != nil { - continue - } - filteredFiles = append(filteredFiles, fileInfo) - } - } - sort.Slice(filteredFiles, func(i, j int) bool { - return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime()) - }) - for i := cast.ToInt(save); i < len(filteredFiles); i++ { - fileToDelete := filepath.Join(path, filteredFiles[i].Name()) - color.Yellow().Printfln("|-" + translate.Get("commands.panel.backup.cleanBackup") + ": " + fileToDelete) - if err := io.Remove(fileToDelete); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error()) - return nil - } - } - color.Green().Printfln("|-" + translate.Get("commands.panel.backup.cleanupSuccess")) - color.Green().Printfln(hr) - color.Green().Printfln("☆ " + translate.Get("commands.panel.backup.success") + " [" + carbon.Now().ToDateTimeString() + "]") - color.Green().Printfln(hr) - - case "cutoff": - name := arg1 - save := arg2 - hr := `+----------------------------------------------------` - if len(name) == 0 || len(save) == 0 { - color.Red().Printfln(translate.Get("commands.panel.cutoff.paramFail")) - return nil - } - - color.Green().Printfln(hr) - color.Green().Printfln("★ " + translate.Get("commands.panel.cutoff.start") + " [" + carbon.Now().ToDateTimeString() + "]") - color.Green().Printfln(hr) - - color.Yellow().Printfln("|-" + translate.Get("commands.panel.cutoff.targetSite") + ": " + name) - var website models.Website - if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.siteNotExist")) - color.Green().Printfln(hr) - return nil - } - - logPath := "/www/wwwlogs/" + website.Name + ".log" - if !io.Exists(logPath) { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.logNotExist")) - color.Green().Printfln(hr) - return nil - } - - backupPath := "/www/wwwlogs/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".log.zip" - if _, err := shell.Execf(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log"); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.backupFail") + ": " + err.Error()) - return nil - } - if _, err := shell.Execf(`echo "" > ` + logPath); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.clearFail") + ": " + err.Error()) - return nil - } - color.Green().Printfln("|-" + translate.Get("commands.panel.cutoff.cutSuccess")) - - color.Green().Printfln(hr) - files, err := os.ReadDir("/www/wwwlogs") - if err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error()) - return nil - } - var filteredFiles []os.FileInfo - for _, file := range files { - if strings.HasPrefix(file.Name(), website.Name) && strings.HasSuffix(file.Name(), ".log.zip") { - fileInfo, err := os.Stat(filepath.Join("/www/wwwlogs", file.Name())) - if err != nil { - continue - } - filteredFiles = append(filteredFiles, fileInfo) - } - } - sort.Slice(filteredFiles, func(i, j int) bool { - return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime()) - }) - for i := cast.ToInt(save); i < len(filteredFiles); i++ { - fileToDelete := filepath.Join("/www/wwwlogs", filteredFiles[i].Name()) - color.Yellow().Printfln("|-" + translate.Get("commands.panel.cutoff.clearLog") + ": " + fileToDelete) - if err := io.Remove(fileToDelete); err != nil { - color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error()) - return nil - } - } - color.Green().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupSuccess")) - color.Green().Printfln(hr) - color.Green().Printfln("☆ " + translate.Get("commands.panel.cutoff.end") + " [" + carbon.Now().ToDateTimeString() + "]") - color.Green().Printfln(hr) - - case "writeSite": - name := arg1 - status := cast.ToBool(arg2) - path := arg3 - php := cast.ToInt(arg4) - ssl := cast.ToBool(ctx.Argument(5)) - if len(name) == 0 || len(path) == 0 { - color.Red().Printfln(translate.Get("commands.panel.writeSite.paramFail")) - return nil - } - - var website models.Website - if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err == nil { - color.Red().Printfln(translate.Get("commands.panel.writeSite.siteExist")) - return nil - } - - _, err := os.Stat(path) - if os.IsNotExist(err) { - color.Red().Printfln(translate.Get("commands.panel.writeSite.pathNotExist")) - return nil - } - - err = facades.Orm().Query().Create(&models.Website{ - Name: name, - Status: status, - Path: path, - PHP: php, - SSL: ssl, - }) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.writeSite.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.writeSite.success")) - - case "deleteSite": - name := arg1 - if len(name) == 0 { - color.Red().Printfln(translate.Get("commands.panel.deleteSite.paramFail")) - return nil - } - - _, err := facades.Orm().Query().Where("name", name).Delete(&models.Website{}) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.deleteSite.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.deleteSite.success")) - - case "writeSetting": - key := arg1 - value := arg2 - if len(key) == 0 || len(value) == 0 { - color.Red().Printfln(translate.Get("commands.panel.writeSetting.paramFail")) - return nil - } - - var setting models.Setting - err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{ - Key: key, - }, models.Setting{ - Value: value, - }) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.writeSetting.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.writeSetting.success")) - - case "getSetting": - key := arg1 - if len(key) == 0 { - color.Red().Printfln(translate.Get("commands.panel.getSetting.paramFail")) - return nil - } - - var setting models.Setting - if err := facades.Orm().Query().Where("key", key).FirstOrFail(&setting); err != nil { - return nil - } - - fmt.Printf("%s", setting.Value) - - case "deleteSetting": - key := arg1 - if len(key) == 0 { - color.Red().Printfln(translate.Get("commands.panel.deleteSetting.paramFail")) - return nil - } - - _, err := facades.Orm().Query().Where("key", key).Delete(&models.Setting{}) - if err != nil { - color.Red().Printfln(translate.Get("commands.panel.deleteSetting.fail")) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.deleteSetting.success")) - - case "addSite": - name := arg1 - domain := arg2 - port := arg3 - path := arg4 - php := arg5 - if len(name) == 0 || len(domain) == 0 || len(port) == 0 || len(path) == 0 { - color.Red().Printfln(translate.Get("commands.panel.addSite.paramFail")) - return nil - } - - domains := strings.Split(domain, ",") - ports := strings.Split(port, ",") - if len(domains) == 0 || len(ports) == 0 { - color.Red().Printfln(translate.Get("commands.panel.addSite.paramFail")) - return nil - } - - var uintPorts []uint - for _, p := range ports { - uintPorts = append(uintPorts, cast.ToUint(p)) - } - - website := services.NewWebsiteImpl() - id, err := website.GetIDByName(name) - if err != nil { - color.Red().Printfln(err.Error()) - return nil - } - if id != 0 { - color.Red().Printfln(translate.Get("commands.panel.addSite.siteExist")) - return nil - } - - _, err = website.Add(requests.Add{ - Name: name, - Domains: domains, - Ports: uintPorts, - Path: path, - PHP: php, - DB: false, - }) - if err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.addSite.success")) - - case "removeSite": - name := arg1 - if len(name) == 0 { - color.Red().Printfln(translate.Get("commands.panel.removeSite.paramFail")) - return nil - } - - website := services.NewWebsiteImpl() - id, err := website.GetIDByName(name) - if err != nil { - color.Red().Printfln(err.Error()) - return nil - } - if id == 0 { - color.Red().Printfln(translate.Get("commands.panel.removeSite.siteNotExist")) - return nil - } - - if err = website.Delete(requests.Delete{ID: id}); err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.removeSite.success")) - - case "installPlugin": - slug := arg1 - if len(slug) == 0 { - color.Red().Printfln(translate.Get("commands.panel.installPlugin.paramFail")) - return nil - } - - plugin := services.NewPluginImpl() - if err := plugin.Install(slug); err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.installPlugin.success")) - - case "uninstallPlugin": - slug := arg1 - if len(slug) == 0 { - color.Red().Printfln(translate.Get("commands.panel.uninstallPlugin.paramFail")) - return nil - } - - plugin := services.NewPluginImpl() - if err := plugin.Uninstall(slug); err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.uninstallPlugin.success")) - - case "updatePlugin": - slug := arg1 - if len(slug) == 0 { - color.Red().Printfln(translate.Get("commands.panel.updatePlugin.paramFail")) - return nil - } - - plugin := services.NewPluginImpl() - if err := plugin.Update(slug); err != nil { - color.Red().Printfln(err.Error()) - return nil - } - - color.Green().Printfln(translate.Get("commands.panel.updatePlugin.success")) - - default: - color.Yellow().Printfln(facades.Config().GetString("panel.name") + " - " + translate.Get("commands.panel.tool") + " - " + facades.Config().GetString("panel.version")) - color.Green().Printfln(translate.Get("commands.panel.use") + ":") - color.Green().Printfln("panel update " + translate.Get("commands.panel.update.description")) - color.Green().Printfln("panel getInfo " + translate.Get("commands.panel.getInfo.description")) - color.Green().Printfln("panel getPort " + translate.Get("commands.panel.getPort.description")) - color.Green().Printfln("panel getEntrance " + translate.Get("commands.panel.getEntrance.description")) - color.Green().Printfln("panel deleteEntrance " + translate.Get("commands.panel.deleteEntrance.description")) - color.Green().Printfln("panel cleanTask " + translate.Get("commands.panel.cleanTask.description")) - color.Green().Printfln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} " + translate.Get("commands.panel.backup.description")) - color.Green().Printfln("panel cutoff {website_name} {save_copies} " + translate.Get("commands.panel.cutoff.description")) - color.Green().Printfln("panel installPlugin {slug} " + translate.Get("commands.panel.installPlugin.description")) - color.Green().Printfln("panel uninstallPlugin {slug} " + translate.Get("commands.panel.uninstallPlugin.description")) - color.Green().Printfln("panel updatePlugin {slug} " + translate.Get("commands.panel.updatePlugin.description")) - color.Green().Printfln("panel addSite {name} {domain} {port} {path} {php} " + translate.Get("commands.panel.addSite.description")) - color.Green().Printfln("panel removeSite {name} " + translate.Get("commands.panel.removeSite.description")) - color.Red().Printfln(translate.Get("commands.panel.forDeveloper") + ":") - color.Yellow().Printfln("panel init " + translate.Get("commands.panel.init.description")) - color.Yellow().Printfln("panel writePlugin {slug} {version} " + translate.Get("commands.panel.writePlugin.description")) - color.Yellow().Printfln("panel deletePlugin {slug} " + translate.Get("commands.panel.deletePlugin.description")) - color.Yellow().Printfln("panel writeMysqlPassword {password} " + translate.Get("commands.panel.writeMysqlPassword.description")) - color.Yellow().Printfln("panel writeSite {name} {status} {path} {php} {ssl} " + translate.Get("commands.panel.writeSite.description")) - color.Yellow().Printfln("panel deleteSite {name} " + translate.Get("commands.panel.deleteSite.description")) - color.Yellow().Printfln("panel getSetting {name} " + translate.Get("commands.panel.getSetting.description")) - color.Yellow().Printfln("panel writeSetting {name} {value} " + translate.Get("commands.panel.writeSetting.description")) - color.Yellow().Printfln("panel deleteSetting {name} " + translate.Get("commands.panel.deleteSetting.description")) - } - - return nil -} diff --git a/app/console/commands/panel_task.go b/app/console/commands/panel_task.go deleted file mode 100644 index f055ccc6..00000000 --- a/app/console/commands/panel_task.go +++ /dev/null @@ -1,79 +0,0 @@ -package commands - -import ( - "context" - "runtime" - "runtime/debug" - - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/types" -) - -// PanelTask 面板每日任务 -type PanelTask struct { -} - -// Signature The name and signature of the console command. -func (receiver *PanelTask) Signature() string { - return "panel:task" -} - -// Description The console command description. -func (receiver *PanelTask) Description() string { - return facades.Lang(context.Background()).Get("commands.panel:task.description") -} - -// Extend The console command extend. -func (receiver *PanelTask) Extend() command.Extend { - return command.Extend{ - Category: "panel", - } -} - -// Handle Execute the console command. -func (receiver *PanelTask) Handle(console.Context) error { - types.Status = types.StatusMaintain - - // 优化数据库 - if _, err := facades.Orm().Query().Exec("VACUUM"); err != nil { - types.Status = types.StatusFailed - facades.Log().Tags("面板", "每日任务"). - With(map[string]any{ - "error": err.Error(), - }).Error("优化面板数据库失败") - return err - } - - // 备份面板 - if err := io.Archive([]string{"/www/panel"}, "/www/backup/panel/panel-"+carbon.Now().ToShortDateTimeString()+".zip"); err != nil { - types.Status = types.StatusFailed - facades.Log().Tags("面板", "每日任务"). - With(map[string]any{ - "error": err.Error(), - }).Error("备份面板失败") - return err - } - - // 清理 7 天前的备份 - if _, err := shell.Execf(`find /www/backup/panel -mtime +7 -name "*.zip" -exec rm -rf {} \;`); err != nil { - types.Status = types.StatusFailed - facades.Log().Tags("面板", "每日任务"). - With(map[string]any{ - "error": err.Error(), - }).Error("清理面板备份失败") - return err - } - - // 回收内存 - runtime.GC() - debug.FreeOSMemory() - - types.Status = types.StatusNormal - return nil -} diff --git a/app/console/kernel.go b/app/console/kernel.go deleted file mode 100644 index 42d977ee..00000000 --- a/app/console/kernel.go +++ /dev/null @@ -1,29 +0,0 @@ -package console - -import ( - "github.com/goravel/framework/contracts/console" - "github.com/goravel/framework/contracts/schedule" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/console/commands" -) - -type Kernel struct { -} - -func (kernel *Kernel) Schedule() []schedule.Event { - return []schedule.Event{ - facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(), - facades.Schedule().Command("panel:cert-renew").DailyAt("04:00").SkipIfStillRunning(), - facades.Schedule().Command("panel:task").DailyAt("03:30").SkipIfStillRunning(), - } -} - -func (kernel *Kernel) Commands() []console.Command { - return []console.Command{ - &commands.Panel{}, - &commands.Monitoring{}, - &commands.CertRenew{}, - &commands.PanelTask{}, - } -} diff --git a/app/http/controllers/cert_controller.go b/app/http/controllers/cert_controller.go deleted file mode 100644 index f148ba5e..00000000 --- a/app/http/controllers/cert_controller.go +++ /dev/null @@ -1,706 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - requests "github.com/TheTNB/panel/v2/app/http/requests/cert" - commonrequests "github.com/TheTNB/panel/v2/app/http/requests/common" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/acme" - "github.com/TheTNB/panel/v2/pkg/h" -) - -type CertController struct { - cron internal.Cron - cert internal.Cert -} - -func NewCertController() *CertController { - return &CertController{ - cron: services.NewCronImpl(), - cert: services.NewCertImpl(), - } -} - -// CAProviders -// -// @Summary 获取 CA 提供商 -// @Description 获取面板证书管理支持的 CA 提供商 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/caProviders [get] -func (r *CertController) CAProviders(ctx http.Context) http.Response { - return h.Success(ctx, []map[string]string{ - { - "name": "Let's Encrypt", - "ca": "letsencrypt", - }, - { - "name": "ZeroSSL", - "ca": "zerossl", - }, - { - "name": "SSL.com", - "ca": "sslcom", - }, - { - "name": "Google", - "ca": "google", - }, - { - "name": "Buypass", - "ca": "buypass", - }, - }) -} - -// DNSProviders -// -// @Summary 获取 DNS 提供商 -// @Description 获取面板证书管理支持的 DNS 提供商 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dnsProviders [get] -func (r *CertController) DNSProviders(ctx http.Context) http.Response { - return h.Success(ctx, []map[string]any{ - { - "name": "DNSPod", - "dns": acme.DnsPod, - }, - { - "name": "腾讯云", - "dns": acme.Tencent, - }, - { - "name": "阿里云", - "dns": acme.AliYun, - }, - { - "name": "CloudFlare", - "dns": acme.CloudFlare, - }, - }) -} - -// Algorithms -// -// @Summary 获取算法列表 -// @Description 获取面板证书管理支持的算法列表 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/algorithms [get] -func (r *CertController) Algorithms(ctx http.Context) http.Response { - return h.Success(ctx, []map[string]any{ - { - "name": "EC256", - "key": acme.KeyEC256, - }, - { - "name": "EC384", - "key": acme.KeyEC384, - }, - { - "name": "RSA2048", - "key": acme.KeyRSA2048, - }, - { - "name": "RSA4096", - "key": acme.KeyRSA4096, - }, - }) -} - -// UserList -// -// @Summary 获取用户列表 -// @Description 获取面板证书管理的 ACME 用户列表 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/users [get] -func (r *CertController) UserList(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - var users []models.CertUser - var total int64 - err := facades.Orm().Query().Paginate(paginateRequest.Page, paginateRequest.Limit, &users, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取ACME用户列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": users, - }) -} - -// UserStore -// -// @Summary 添加 ACME 用户 -// @Description 添加 ACME 用户到面板证书管理 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.UserStore true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/users [post] -func (r *CertController) UserStore(ctx http.Context) http.Response { - var storeRequest requests.UserStore - sanitize := h.SanitizeRequest(ctx, &storeRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.UserStore(storeRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("添加ACME用户失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UserUpdate -// -// @Summary 更新 ACME 用户 -// @Description 更新面板证书管理的 ACME 用户 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "用户 ID" -// @Param data body requests.UserUpdate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/users/{id} [put] -func (r *CertController) UserUpdate(ctx http.Context) http.Response { - var updateRequest requests.UserUpdate - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.UserUpdate(updateRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "userID": updateRequest.ID, - "error": err.Error(), - }).Info("更新ACME用户失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UserShow -// -// @Summary 获取 ACME 用户 -// @Description 获取面板证书管理的 ACME 用户 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param id path int true "用户 ID" -// @Success 200 {object} SuccessResponse{data=models.CertUser} -// @Router /panel/cert/users/{id} [get] -func (r *CertController) UserShow(ctx http.Context) http.Response { - var showAndDestroyRequest requests.UserShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - user, err := r.cert.UserShow(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "userID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("获取ACME用户失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, user) -} - -// UserDestroy -// -// @Summary 删除 ACME 用户 -// @Description 删除面板证书管理的 ACME 用户 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "用户 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/users/{id} [delete] -func (r *CertController) UserDestroy(ctx http.Context) http.Response { - var showAndDestroyRequest requests.UserShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.UserDestroy(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "userID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("删除ACME用户失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DNSList -// -// @Summary 获取 DNS 接口列表 -// @Description 获取面板证书管理的 DNS 接口列表 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dns [get] -func (r *CertController) DNSList(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - var dns []models.CertDNS - var total int64 - err := facades.Orm().Query().Paginate(paginateRequest.Page, paginateRequest.Limit, &dns, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取DNS接口列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": dns, - }) -} - -// DNSStore -// -// @Summary 添加 DNS 接口 -// @Description 添加 DNS 接口到面板证书管理 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.DNSStore true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dns [post] -func (r *CertController) DNSStore(ctx http.Context) http.Response { - var storeRequest requests.DNSStore - sanitize := h.SanitizeRequest(ctx, &storeRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.DNSStore(storeRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("添加DNS接口失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// DNSShow -// -// @Summary 获取 DNS 接口 -// @Description 获取面板证书管理的 DNS 接口 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param id path int true "DNS 接口 ID" -// @Success 200 {object} SuccessResponse{data=models.CertDNS} -// @Router /panel/cert/dns/{id} [get] -func (r *CertController) DNSShow(ctx http.Context) http.Response { - var showAndDestroyRequest requests.DNSShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - dns, err := r.cert.DNSShow(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "dnsID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("获取DNS接口失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, dns) -} - -// DNSUpdate -// -// @Summary 更新 DNS 接口 -// @Description 更新面板证书管理的 DNS 接口 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "DNS 接口 ID" -// @Param data body requests.DNSUpdate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dns/{id} [put] -func (r *CertController) DNSUpdate(ctx http.Context) http.Response { - var updateRequest requests.DNSUpdate - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.DNSUpdate(updateRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "dnsID": updateRequest.ID, - "error": err.Error(), - }).Info("更新DNS接口失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// DNSDestroy -// -// @Summary 删除 DNS 接口 -// @Description 删除面板证书管理的 DNS 接口 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "DNS 接口 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/dns/{id} [delete] -func (r *CertController) DNSDestroy(ctx http.Context) http.Response { - var showAndDestroyRequest requests.DNSShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.DNSDestroy(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "dnsID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("删除DNS接口失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// CertList -// -// @Summary 获取证书列表 -// @Description 获取面板证书管理的证书列表 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/certs [get] -func (r *CertController) CertList(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - var certs []models.Cert - var total int64 - err := facades.Orm().Query().With("Website").With("User").With("DNS").Paginate(paginateRequest.Page, paginateRequest.Limit, &certs, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取证书列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": certs, - }) -} - -// CertStore -// -// @Summary 添加证书 -// @Description 添加证书到面板证书管理 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.CertStore true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/certs [post] -func (r *CertController) CertStore(ctx http.Context) http.Response { - var storeRequest requests.CertStore - sanitize := h.SanitizeRequest(ctx, &storeRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.CertStore(storeRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("添加证书失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// CertUpdate -// -// @Summary 更新证书 -// @Description 更新面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "证书 ID" -// @Param data body requests.CertUpdate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/certs/{id} [put] -func (r *CertController) CertUpdate(ctx http.Context) http.Response { - var updateRequest requests.CertUpdate - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.CertUpdate(updateRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": updateRequest.ID, - "error": err.Error(), - }).Info("更新证书失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// CertShow -// -// @Summary 获取证书 -// @Description 获取面板证书管理的证书 -// @Tags TLS证书 -// @Produce json -// @Security BearerToken -// @Param id path int true "证书 ID" -// @Success 200 {object} SuccessResponse{data=models.Cert} -// @Router /panel/cert/certs/{id} [get] -func (r *CertController) CertShow(ctx http.Context) http.Response { - var showAndDestroyRequest requests.CertShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - cert, err := r.cert.CertShow(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("获取证书失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, cert) -} - -// CertDestroy -// -// @Summary 删除证书 -// @Description 删除面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "证书 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/certs/{id} [delete] -func (r *CertController) CertDestroy(ctx http.Context) http.Response { - var showAndDestroyRequest requests.CertShowAndDestroy - sanitize := h.SanitizeRequest(ctx, &showAndDestroyRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.CertDestroy(showAndDestroyRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": showAndDestroyRequest.ID, - "error": err.Error(), - }).Info("删除证书失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// Obtain -// -// @Summary 签发证书 -// @Description 签发面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Obtain true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/obtain [post] -func (r *CertController) Obtain(ctx http.Context) http.Response { - var obtainRequest requests.Obtain - sanitize := h.SanitizeRequest(ctx, &obtainRequest) - if sanitize != nil { - return sanitize - } - - cert, err := r.cert.CertShow(obtainRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": obtainRequest.ID, - "error": err.Error(), - }).Info("获取证书失败") - return h.ErrorSystem(ctx) - } - - if cert.DNS != nil || cert.Website != nil { - _, err = r.cert.ObtainAuto(obtainRequest.ID) - } else { - _, err = r.cert.ObtainManual(obtainRequest.ID) - } - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("签发证书失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Renew -// -// @Summary 续签证书 -// @Description 续签面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Renew true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/renew [post] -func (r *CertController) Renew(ctx http.Context) http.Response { - var renewRequest requests.Renew - sanitize := h.SanitizeRequest(ctx, &renewRequest) - if sanitize != nil { - return sanitize - } - - _, err := r.cert.Renew(renewRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("续签证书失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ManualDNS -// -// @Summary 获取手动 DNS 记录 -// @Description 获取签发证书所需的 DNS 记录 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Obtain true "request" -// @Success 200 {object} SuccessResponse{data=[]acme.DNSRecord} -// @Router /panel/cert/manualDNS [post] -func (r *CertController) ManualDNS(ctx http.Context) http.Response { - var obtainRequest requests.Obtain - sanitize := h.SanitizeRequest(ctx, &obtainRequest) - if sanitize != nil { - return sanitize - } - - resolves, err := r.cert.ManualDNS(obtainRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取手动DNS记录失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, resolves) -} - -// Deploy -// -// @Summary 部署证书 -// @Description 部署面板证书管理的证书 -// @Tags TLS证书 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.CertDeploy true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/cert/deploy [post] -func (r *CertController) Deploy(ctx http.Context) http.Response { - var deployRequest requests.CertDeploy - sanitize := h.SanitizeRequest(ctx, &deployRequest) - if sanitize != nil { - return sanitize - } - - err := r.cert.Deploy(deployRequest.ID, deployRequest.WebsiteID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{ - "certID": deployRequest.ID, - "error": err.Error(), - }).Info("部署证书失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/container_controller.go b/app/http/controllers/container_controller.go deleted file mode 100644 index be82d3ba..00000000 --- a/app/http/controllers/container_controller.go +++ /dev/null @@ -1,1011 +0,0 @@ -package controllers - -import ( - "fmt" - "strconv" - "strings" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/go-connections/nat" - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/support/carbon" - - requests "github.com/TheTNB/panel/v2/app/http/requests/container" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type ContainerController struct { - container services.Container -} - -func NewContainerController() *ContainerController { - return &ContainerController{ - container: services.NewContainer(), - } -} - -// ContainerList -// -// @Summary 获取容器列表 -// @Description 获取所有容器列表 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/list [get] -func (r *ContainerController) ContainerList(ctx http.Context) http.Response { - containers, err := r.container.ContainerListAll() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, containers) - - items := make([]any, 0) - for _, item := range paged { - var name string - if len(item.Names) > 0 { - name = item.Names[0] - } - items = append(items, map[string]any{ - "id": item.ID, - "name": strings.TrimLeft(name, "/"), - "image": item.Image, - "image_id": item.ImageID, - "command": item.Command, - "created": carbon.FromTimestamp(item.Created).ToDateTimeString(), - "ports": item.Ports, - "labels": item.Labels, - "state": item.State, - "status": item.Status, - }) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": items, - }) -} - -// ContainerSearch -// -// @Summary 搜索容器 -// @Description 根据容器名称搜索容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param name query string true "容器名称" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/search [get] -func (r *ContainerController) ContainerSearch(ctx http.Context) http.Response { - fields := strings.Fields(ctx.Request().Query("name")) - containers, err := r.container.ContainerListByNames(fields) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, containers) -} - -// ContainerCreate -// -// @Summary 创建容器 -// @Description 创建一个容器 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.ContainerCreate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/create [post] -func (r *ContainerController) ContainerCreate(ctx http.Context) http.Response { - var request requests.ContainerCreate - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - var hostConf container.HostConfig - var networkConf network.NetworkingConfig - - portMap := make(nat.PortMap) - for _, port := range request.Ports { - if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd { - return h.Error(ctx, http.StatusUnprocessableEntity, fmt.Sprintf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd)) - } - if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 { - return h.Error(ctx, http.StatusUnprocessableEntity, "端口范围不正确") - } - - count := 0 - for host := port.HostStart; host <= port.HostEnd; host++ { - bindItem := nat.PortBinding{HostPort: strconv.Itoa(host), HostIP: port.Host} - portMap[nat.Port(fmt.Sprintf("%d/%s", port.ContainerStart+count, port.Protocol))] = []nat.PortBinding{bindItem} - count++ - } - } - - exposed := make(nat.PortSet) - for port := range portMap { - exposed[port] = struct{}{} - } - - if request.Network != "" { - switch request.Network { - case "host", "none", "bridge": - hostConf.NetworkMode = container.NetworkMode(request.Network) - } - networkConf.EndpointsConfig = map[string]*network.EndpointSettings{request.Network: {}} - } else { - networkConf = network.NetworkingConfig{} - } - - hostConf.Privileged = request.Privileged - hostConf.AutoRemove = request.AutoRemove - hostConf.CPUShares = request.CPUShares - hostConf.PublishAllPorts = request.PublishAllPorts - hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(request.RestartPolicy)} - if request.RestartPolicy == "on-failure" { - hostConf.RestartPolicy.MaximumRetryCount = 5 - } - hostConf.NanoCPUs = request.CPUs * 1000000000 - hostConf.Memory = request.Memory * 1024 * 1024 - hostConf.MemorySwap = 0 - hostConf.PortBindings = portMap - hostConf.Binds = []string{} - - volumes := make(map[string]struct{}) - for _, v := range request.Volumes { - volumes[v.Container] = struct{}{} - hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", v.Host, v.Container, v.Mode)) - } - - id, err := r.container.ContainerCreate(request.Name, - container.Config{ - Image: request.Image, - Env: r.container.KVToSlice(request.Env), - Entrypoint: request.Entrypoint, - Cmd: request.Command, - Labels: r.container.KVToMap(request.Labels), - ExposedPorts: exposed, - OpenStdin: request.OpenStdin, - Tty: request.Tty, - Volumes: volumes, - }, - hostConf, - networkConf, - ) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = r.container.ContainerStart(id); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, id) -} - -// ContainerRemove -// -// @Summary 删除容器 -// @Description 删除一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/remove [post] -func (r *ContainerController) ContainerRemove(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerRemove(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerStart -// -// @Summary 启动容器 -// @Description 启动一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/start [post] -func (r *ContainerController) ContainerStart(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerStart(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerStop -// -// @Summary 停止容器 -// @Description 停止一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/stop [post] -func (r *ContainerController) ContainerStop(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerStop(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerRestart -// -// @Summary 重启容器 -// @Description 重启一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/restart [post] -func (r *ContainerController) ContainerRestart(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerRestart(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerPause -// -// @Summary 暂停容器 -// @Description 暂停一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -func (r *ContainerController) ContainerPause(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerPause(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerUnpause -// -// @Summary 取消暂停容器 -// @Description 取消暂停一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// -// @Router /panel/container/unpause [post] -func (r *ContainerController) ContainerUnpause(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerUnpause(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerInspect -// -// @Summary 查看容器 -// @Description 查看一个容器的详细信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/inspect [get] -func (r *ContainerController) ContainerInspect(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.ContainerInspect(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// ContainerKill -// -// @Summary 杀死容器 -// @Description 杀死一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/kill [post] -func (r *ContainerController) ContainerKill(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerKill(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerRename -// -// @Summary 重命名容器 -// @Description 重命名一个容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ContainerRename true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/rename [post] -func (r *ContainerController) ContainerRename(ctx http.Context) http.Response { - var request requests.ContainerRename - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ContainerRename(request.ID, request.Name); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ContainerStats -// -// @Summary 查看容器状态 -// @Description 查看一个容器的状态信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/stats [get] -func (r *ContainerController) ContainerStats(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.ContainerStats(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// ContainerExist -// -// @Summary 检查容器是否存在 -// @Description 检查一个容器是否存在 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/exist [get] -func (r *ContainerController) ContainerExist(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - exist, err := r.container.ContainerExist(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, exist) -} - -// ContainerLogs -// -// @Summary 查看容器日志 -// @Description 查看一个容器的日志 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/logs [get] -func (r *ContainerController) ContainerLogs(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.ContainerLogs(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// ContainerPrune -// -// @Summary 清理容器 -// @Description 清理无用的容器 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/container/prune [post] -func (r *ContainerController) ContainerPrune(ctx http.Context) http.Response { - if err := r.container.ContainerPrune(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// NetworkList -// -// @Summary 获取网络列表 -// @Description 获取所有网络列表 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/list [get] -func (r *ContainerController) NetworkList(ctx http.Context) http.Response { - networks, err := r.container.NetworkList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, networks) - - items := make([]any, 0) - for _, item := range paged { - var ipamConfig []any - for _, v := range item.IPAM.Config { - ipamConfig = append(ipamConfig, map[string]any{ - "subnet": v.Subnet, - "gateway": v.Gateway, - "ip_range": v.IPRange, - "aux_address": v.AuxAddress, - }) - } - items = append(items, map[string]any{ - "id": item.ID, - "name": item.Name, - "driver": item.Driver, - "ipv6": item.EnableIPv6, - "scope": item.Scope, - "internal": item.Internal, - "attachable": item.Attachable, - "ingress": item.Ingress, - "labels": item.Labels, - "options": item.Options, - "ipam": map[string]any{ - "config": ipamConfig, - "driver": item.IPAM.Driver, - "options": item.IPAM.Options, - }, - "created": carbon.FromStdTime(item.Created).ToDateTimeString(), - }) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": items, - }) -} - -// NetworkCreate -// -// @Summary 创建网络 -// @Description 创建一个网络 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NetworkCreate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/create [post] -func (r *ContainerController) NetworkCreate(ctx http.Context) http.Response { - var request requests.NetworkCreate - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - id, err := r.container.NetworkCreate(request) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, id) -} - -// NetworkRemove -// -// @Summary 删除网络 -// @Description 删除一个网络 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/remove [post] -func (r *ContainerController) NetworkRemove(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.NetworkRemove(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// NetworkExist -// -// @Summary 检查网络是否存在 -// @Description 检查一个网络是否存在 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/exist [get] -func (r *ContainerController) NetworkExist(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - exist, err := r.container.NetworkExist(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, exist) -} - -// NetworkInspect -// -// @Summary 查看网络 -// @Description 查看一个网络的详细信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/inspect [get] -func (r *ContainerController) NetworkInspect(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.NetworkInspect(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// NetworkConnect -// -// @Summary 连接容器到网络 -// @Description 连接一个容器到一个网络 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NetworkConnectDisConnect true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/connect [post] -func (r *ContainerController) NetworkConnect(ctx http.Context) http.Response { - var request requests.NetworkConnectDisConnect - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.NetworkConnect(request.Network, request.Container); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// NetworkDisconnect -// -// @Summary 从网络断开容器 -// @Description 从一个网络断开一个容器 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NetworkConnectDisConnect true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/disconnect [post] -func (r *ContainerController) NetworkDisconnect(ctx http.Context) http.Response { - var request requests.NetworkConnectDisConnect - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.NetworkDisconnect(request.Network, request.Container); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// NetworkPrune -// -// @Summary 清理网络 -// @Description 清理无用的网络 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/container/network/prune [post] -func (r *ContainerController) NetworkPrune(ctx http.Context) http.Response { - if err := r.container.NetworkPrune(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ImageList -// -// @Summary 获取镜像列表 -// @Description 获取所有镜像列表 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/list [get] -func (r *ContainerController) ImageList(ctx http.Context) http.Response { - images, err := r.container.ImageList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, images) - - items := make([]any, 0) - for _, item := range paged { - items = append(items, map[string]any{ - "id": item.ID, - "created": carbon.FromTimestamp(item.Created).ToDateTimeString(), - "containers": item.Containers, - "size": str.FormatBytes(float64(item.Size)), - "labels": item.Labels, - "repo_tags": item.RepoTags, - "repo_digests": item.RepoDigests, - }) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": items, - }) -} - -// ImageExist -// -// @Summary 检查镜像是否存在 -// @Description 检查一个镜像是否存在 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/exist [get] -func (r *ContainerController) ImageExist(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - exist, err := r.container.ImageExist(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, exist) -} - -// ImagePull -// -// @Summary 拉取镜像 -// @Description 拉取一个镜像 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.ImagePull true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/pull [post] -func (r *ContainerController) ImagePull(ctx http.Context) http.Response { - var request requests.ImagePull - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ImagePull(request); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ImageRemove -// -// @Summary 删除镜像 -// @Description 删除一个镜像 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/remove [post] -func (r *ContainerController) ImageRemove(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.ImageRemove(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ImagePrune -// -// @Summary 清理镜像 -// @Description 清理无用的镜像 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/prune [post] -func (r *ContainerController) ImagePrune(ctx http.Context) http.Response { - if err := r.container.ImagePrune(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ImageInspect -// -// @Summary 查看镜像 -// @Description 查看一个镜像的详细信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/image/inspect [get] -func (r *ContainerController) ImageInspect(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.ImageInspect(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// VolumeList -// -// @Summary 获取卷列表 -// @Description 获取所有卷列表 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/list [get] -func (r *ContainerController) VolumeList(ctx http.Context) http.Response { - volumes, err := r.container.VolumeList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, volumes) - - items := make([]any, 0) - for _, item := range paged { - var usage any - if item.UsageData != nil { - usage = map[string]any{ - "ref_count": item.UsageData.RefCount, - "size": str.FormatBytes(float64(item.UsageData.Size)), - } - } - items = append(items, map[string]any{ - "id": item.Name, - "created": carbon.Parse(item.CreatedAt).ToDateTimeString(), - "driver": item.Driver, - "mount": item.Mountpoint, - "labels": item.Labels, - "options": item.Options, - "scope": item.Scope, - "status": item.Status, - "usage": usage, - }) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": items, - }) -} - -// VolumeCreate -// -// @Summary 创建卷 -// @Description 创建一个卷 -// @Tags 容器 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.VolumeCreate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/create [post] -func (r *ContainerController) VolumeCreate(ctx http.Context) http.Response { - var request requests.VolumeCreate - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.VolumeCreate(request) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data.Name) -} - -// VolumeExist -// -// @Summary 检查卷是否存在 -// @Description 检查一个卷是否存在 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/exist [get] -func (r *ContainerController) VolumeExist(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - exist, err := r.container.VolumeExist(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, exist) -} - -// VolumeInspect -// -// @Summary 查看卷 -// @Description 查看一个卷的详细信息 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data query requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/inspect [get] -func (r *ContainerController) VolumeInspect(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - data, err := r.container.VolumeInspect(request.ID) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, data) -} - -// VolumeRemove -// -// @Summary 删除卷 -// @Description 删除一个卷 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Param data body requests.ID true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/remove [post] -func (r *ContainerController) VolumeRemove(ctx http.Context) http.Response { - var request requests.ID - if sanitize := h.SanitizeRequest(ctx, &request); sanitize != nil { - return sanitize - } - - if err := r.container.VolumeRemove(request.ID); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// VolumePrune -// -// @Summary 清理卷 -// @Description 清理无用的卷 -// @Tags 容器 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/container/volume/prune [post] -func (r *ContainerController) VolumePrune(ctx http.Context) http.Response { - if err := r.container.VolumePrune(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/cron_controller.go b/app/http/controllers/cron_controller.go deleted file mode 100644 index 7b5c76c3..00000000 --- a/app/http/controllers/cron_controller.go +++ /dev/null @@ -1,296 +0,0 @@ -package controllers - -import ( - "regexp" - "strconv" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type CronController struct { - cron internal.Cron - setting internal.Setting -} - -func NewCronController() *CronController { - return &CronController{ - cron: services.NewCronImpl(), - setting: services.NewSettingImpl(), - } -} - -// List 获取计划任务列表 -func (r *CronController) List(ctx http.Context) http.Response { - limit := ctx.Request().QueryInt("limit", 10) - page := ctx.Request().QueryInt("page", 1) - - var crons []models.Cron - var total int64 - err := facades.Orm().Query().Paginate(page, limit, &crons, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("查询计划任务列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": crons, - }) -} - -// Add 添加计划任务 -func (r *CronController) Add(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|min_len:1|max_len:255", - "time": "required", - "script": "required", - "type": "required|in:shell,backup,cutoff", - "backup_type": "required_if:type,backup|in:website,mysql,postgresql", - }); sanitize != nil { - return sanitize - } - - // 单独验证时间格式 - if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) { - return h.Error(ctx, http.StatusUnprocessableEntity, "时间格式错误") - } - - script := ctx.Request().Input("script") - cronType := ctx.Request().Input("type") - if cronType == "backup" { - backupType := ctx.Request().Input("backup_type") - backupName := ctx.Request().Input("database") - if backupType == "website" { - backupName = ctx.Request().Input("website") - } - backupPath := ctx.Request().Input("backup_path") - if len(backupPath) == 0 { - backupPath = r.setting.Get(models.SettingKeyBackupPath) + "/" + backupType - } - backupSave := ctx.Request().InputInt("save", 10) - script = `#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -# 耗子面板 - 数据备份脚本 - -type=` + backupType + ` -path=` + backupPath + ` -name=` + backupName + ` -save=` + cast.ToString(backupSave) + ` - -# 执行备份 -panel backup ${type} ${name} ${path} ${save} 2>&1 -` - } - if cronType == "cutoff" { - website := ctx.Request().Input("website") - save := ctx.Request().InputInt("save", 180) - script = `#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -# 耗子面板 - 日志切割脚本 - -name=` + website + ` -save=` + cast.ToString(save) + ` - -# 执行切割 -panel cutoff ${name} ${save} 2>&1 -` - } - - shellDir := "/www/server/cron/" - shellLogDir := "/www/server/cron/logs/" - if !io.Exists(shellDir) { - return h.Error(ctx, http.StatusInternalServerError, "计划任务目录不存在") - } - if !io.Exists(shellLogDir) { - return h.Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在") - } - shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + str.RandomString(16) - if err := io.Write(shellDir+shellFile+".sh", script, 0700); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if out, err := shell.Execf("dos2unix " + shellDir + shellFile + ".sh"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - var cron models.Cron - cron.Name = ctx.Request().Input("name") - cron.Type = ctx.Request().Input("type") - cron.Status = true - cron.Time = ctx.Request().Input("time") - cron.Shell = shellDir + shellFile + ".sh" - cron.Log = shellLogDir + shellFile + ".log" - - if err := facades.Orm().Query().Create(&cron); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("保存计划任务失败") - return h.ErrorSystem(ctx) - } - - if err := r.cron.AddToSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, http.Json{ - "id": cron.ID, - }) -} - -// Script 获取脚本内容 -func (r *CronController) Script(ctx http.Context) http.Response { - var cron models.Cron - err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - script, err := io.Read(cron.Shell) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, script) -} - -// Update 更新计划任务 -func (r *CronController) Update(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|min_len:1|max_len:255", - "time": "required", - "script": "required", - }); sanitize != nil { - return sanitize - } - - // 单独验证时间格式 - if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) { - return h.Error(ctx, http.StatusUnprocessableEntity, "时间格式错误") - } - - var cron models.Cron - if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - if !cron.Status { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务已禁用") - } - - cron.Time = ctx.Request().Input("time") - cron.Name = ctx.Request().Input("name") - if err := facades.Orm().Query().Save(&cron); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("更新计划任务失败") - return h.ErrorSystem(ctx) - } - - if err := io.Write(cron.Shell, ctx.Request().Input("script"), 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if out, err := shell.Execf("dos2unix " + cron.Shell); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := r.cron.DeleteFromSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if cron.Status { - if err := r.cron.AddToSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - return h.Success(ctx, nil) -} - -// Delete 删除计划任务 -func (r *CronController) Delete(ctx http.Context) http.Response { - var cron models.Cron - if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - if err := r.cron.DeleteFromSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := io.Remove(cron.Shell); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if _, err := facades.Orm().Query().Delete(&cron); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("删除计划任务失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// Status 更新计划任务状态 -func (r *CronController) Status(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "status": "bool", - }); sanitize != nil { - return sanitize - } - - var cron models.Cron - if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - cron.Status = ctx.Request().InputBool("status") - if err := facades.Orm().Query().Save(&cron); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{ - "error": err.Error(), - }).Info("更新计划任务状态失败") - return h.ErrorSystem(ctx) - } - - if err := r.cron.DeleteFromSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if cron.Status { - if err := r.cron.AddToSystem(cron); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - return h.Success(ctx, nil) -} - -// Log 获取计划任务日志 -func (r *CronController) Log(ctx http.Context) http.Response { - var cron models.Cron - if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在") - } - - if !io.Exists(cron.Log) { - return h.Error(ctx, http.StatusUnprocessableEntity, "日志文件不存在") - } - - log, err := shell.Execf("tail -n 1000 " + cron.Log) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, log) -} diff --git a/app/http/controllers/file_controller.go b/app/http/controllers/file_controller.go deleted file mode 100644 index d85e81d8..00000000 --- a/app/http/controllers/file_controller.go +++ /dev/null @@ -1,519 +0,0 @@ -package controllers - -import ( - "fmt" - stdio "io" - stdos "os" - "path/filepath" - "strconv" - "strings" - "syscall" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/support/carbon" - - requests "github.com/TheTNB/panel/v2/app/http/requests/file" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type FileController struct { -} - -func NewFileController() *FileController { - return &FileController{} -} - -// Create -// -// @Summary 创建文件/目录 -// @Description 创建文件/目录到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NotExist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/create [post] -func (r *FileController) Create(ctx http.Context) http.Response { - var request requests.NotExist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - isDir := ctx.Request().InputBool("dir") - if !isDir { - if out, err := shell.Execf("touch " + request.Path); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if err := io.Mkdir(request.Path, 0755); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - r.setPermission(request.Path, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Content -// -// @Summary 获取文件内容 -// @Description 获取给定路径的文件内容 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data query requests.Exist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/content [get] -func (r *FileController) Content(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - fileInfo, err := io.FileInfo(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if fileInfo.IsDir() { - return h.Error(ctx, http.StatusInternalServerError, "目标路径不是文件") - } - if fileInfo.Size() > 10*1024*1024 { - return h.Error(ctx, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑") - } - - content, err := io.Read(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, content) -} - -// Save -// -// @Summary 保存文件内容 -// @Description 保存给定路径的文件内容 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Save true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/save [post] -func (r *FileController) Save(ctx http.Context) http.Response { - var request requests.Save - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - fileInfo, err := io.FileInfo(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = io.Write(request.Path, request.Content, fileInfo.Mode()); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Path, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Delete -// -// @Summary 删除文件/目录 -// @Description 删除给定路径的文件/目录 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Exist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/delete [post] -func (r *FileController) Delete(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if err := io.Remove(request.Path); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Upload -// -// @Summary 上传文件 -// @Description 上传文件到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param file formData file true "file" -// @Param path formData string true "path" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/upload [post] -func (r *FileController) Upload(ctx http.Context) http.Response { - var request requests.Upload - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - src, err := request.File.Open() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - defer src.Close() - - if io.Exists(request.Path) && !ctx.Request().InputBool("force") { - return h.Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?") - } - - data, err := stdio.ReadAll(src) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = io.Write(request.Path, string(data), 0755); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Path, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Move -// -// @Summary 移动文件/目录 -// @Description 移动文件/目录到给定路径,等效于重命名 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Move true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/move [post] -func (r *FileController) Move(ctx http.Context) http.Response { - var request requests.Move - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if io.Exists(request.Target) && !ctx.Request().InputBool("force") { - return h.Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在") - } - - if err := io.Mv(request.Source, request.Target); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Target, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Copy -// -// @Summary 复制文件/目录 -// @Description 复制文件/目录到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Copy true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/copy [post] -func (r *FileController) Copy(ctx http.Context) http.Response { - var request requests.Copy - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if io.Exists(request.Target) && !ctx.Request().InputBool("force") { - return h.Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在") - } - - if err := io.Cp(request.Source, request.Target); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Source, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Download -// -// @Summary 下载文件 -// @Description 下载给定路径的文件 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data query requests.NotExist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/download [get] -func (r *FileController) Download(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - info, err := io.FileInfo(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if info.IsDir() { - return h.Error(ctx, http.StatusInternalServerError, "不能下载目录") - } - - return ctx.Response().Download(request.Path, info.Name()) -} - -// RemoteDownload -// -// @Summary 下载远程文件 -// @Description 下载远程文件到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.NotExist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/remoteDownload [post] -func (r *FileController) RemoteDownload(ctx http.Context) http.Response { - var request requests.NotExist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - // TODO 使用异步任务下载文件 - return nil -} - -// Info -// -// @Summary 获取文件/目录信息 -// @Description 获取给定路径的文件/目录信息 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data query requests.Exist true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/info [get] -func (r *FileController) Info(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - fileInfo, err := io.FileInfo(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, http.Json{ - "name": fileInfo.Name(), - "size": str.FormatBytes(float64(fileInfo.Size())), - "mode_str": fileInfo.Mode().String(), - "mode": fmt.Sprintf("%04o", fileInfo.Mode().Perm()), - "dir": fileInfo.IsDir(), - "modify": carbon.FromStdTime(fileInfo.ModTime()).ToDateTimeString(), - }) -} - -// Permission -// -// @Summary 修改文件/目录权限 -// @Description 修改给定路径的文件/目录权限 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Permission true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/permission [post] -func (r *FileController) Permission(ctx http.Context) http.Response { - var request requests.Permission - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - // 解析成8进制 - mode, err := strconv.ParseUint(request.Mode, 8, 64) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = io.Chmod(request.Path, stdos.FileMode(mode)); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = io.Chown(request.Path, request.Owner, request.Group); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Archive -// -// @Summary 压缩文件/目录 -// @Description 压缩文件/目录到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Archive true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/archive [post] -func (r *FileController) Archive(ctx http.Context) http.Response { - var request requests.Archive - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if err := io.Archive(request.Paths, request.File); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.File, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// UnArchive -// -// @Summary 解压文件/目录 -// @Description 解压文件/目录到给定路径 -// @Tags 文件 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.UnArchive true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/unArchive [post] -func (r *FileController) UnArchive(ctx http.Context) http.Response { - var request requests.UnArchive - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - if err := io.UnArchive(request.File, request.Path); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - r.setPermission(request.Path, 0755, "www", "www") - return h.Success(ctx, nil) -} - -// Search -// -// @Summary 搜索文件/目录 -// @Description 通过关键词搜索给定路径的文件/目录 -// @Tags 文件 -// @Accept json -// @Produce json -// @Param data body requests.Search true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/search [post] -func (r *FileController) Search(ctx http.Context) http.Response { - var request requests.Search - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - paths := make(map[string]stdos.FileInfo) - err := filepath.Walk(request.Path, func(path string, info stdos.FileInfo, err error) error { - if err != nil { - return err - } - if strings.Contains(info.Name(), request.KeyWord) { - paths[path] = info - } - return nil - }) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, paths) -} - -// List -// -// @Summary 获取文件/目录列表 -// @Description 获取给定路径的文件/目录列表 -// @Tags 文件 -// @Accept json -// @Produce json -// @Param data query requests.Exist true "request" -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/file/list [get] -func (r *FileController) List(ctx http.Context) http.Response { - var request requests.Exist - sanitize := h.SanitizeRequest(ctx, &request) - if sanitize != nil { - return sanitize - } - - fileInfoList, err := io.ReadDir(request.Path) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - var paths []any - for _, fileInfo := range fileInfoList { - info, _ := fileInfo.Info() - stat := info.Sys().(*syscall.Stat_t) - - paths = append(paths, map[string]any{ - "name": info.Name(), - "full": filepath.Join(request.Path, info.Name()), - "size": str.FormatBytes(float64(info.Size())), - "mode_str": info.Mode().String(), - "mode": fmt.Sprintf("%04o", info.Mode().Perm()), - "owner": os.GetUser(stat.Uid), - "group": os.GetGroup(stat.Gid), - "uid": stat.Uid, - "gid": stat.Gid, - "hidden": io.IsHidden(info.Name()), - "symlink": io.IsSymlink(info.Mode()), - "link": io.GetSymlink(filepath.Join(request.Path, info.Name())), - "dir": info.IsDir(), - "modify": carbon.FromStdTime(info.ModTime()).ToDateTimeString(), - }) - } - - paged, total := h.Paginate(ctx, paths) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// setPermission -func (r *FileController) setPermission(path string, mode stdos.FileMode, owner, group string) { - _ = io.Chmod(path, mode) - _ = io.Chown(path, owner, group) -} diff --git a/app/http/controllers/info_controller.go b/app/http/controllers/info_controller.go deleted file mode 100644 index b3c07812..00000000 --- a/app/http/controllers/info_controller.go +++ /dev/null @@ -1,361 +0,0 @@ -package controllers - -import ( - "database/sql" - "fmt" - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/hashicorp/go-version" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/tools" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type MenuItem struct { - Name string `json:"name"` - Title string `json:"title"` - Icon string `json:"icon"` - Jump string `json:"jump"` -} - -type InfoController struct { - plugin internal.Plugin - setting internal.Setting -} - -func NewInfoController() *InfoController { - return &InfoController{ - plugin: services.NewPluginImpl(), - setting: services.NewSettingImpl(), - } -} - -// Panel 获取面板信息 -func (r *InfoController) Panel(ctx http.Context) http.Response { - return h.Success(ctx, http.Json{ - "name": r.setting.Get(models.SettingKeyName), - "language": facades.Config().GetString("app.locale"), - }) -} - -// HomePlugins 获取首页插件 -func (r *InfoController) HomePlugins(ctx http.Context) http.Response { - var plugins []models.Plugin - err := facades.Orm().Query().Where("show", 1).Find(&plugins) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("获取首页插件失败") - return h.ErrorSystem(ctx) - } - - type pluginsData struct { - models.Plugin - Name string `json:"name"` - } - - var pluginsJson []pluginsData - for _, plugin := range plugins { - pluginsJson = append(pluginsJson, pluginsData{ - Plugin: plugin, - Name: r.plugin.GetBySlug(plugin.Slug).Name, - }) - } - - return h.Success(ctx, pluginsJson) -} - -// NowMonitor 获取当前监控信息 -func (r *InfoController) NowMonitor(ctx http.Context) http.Response { - return h.Success(ctx, tools.GetMonitoringInfo()) -} - -// SystemInfo 获取系统信息 -func (r *InfoController) SystemInfo(ctx http.Context) http.Response { - monitorInfo := tools.GetMonitoringInfo() - - return h.Success(ctx, http.Json{ - "os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion, - "uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400), - "panel_version": facades.Config().GetString("panel.version"), - }) -} - -// CountInfo 获取面板统计信息 -func (r *InfoController) CountInfo(ctx http.Context) http.Response { - var websiteCount int64 - err := facades.Orm().Query().Model(models.Website{}).Count(&websiteCount) - if err != nil { - websiteCount = -1 - } - - var mysql models.Plugin - mysqlInstalled := true - err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql) - if err != nil { - mysqlInstalled = false - } - var postgresql models.Plugin - postgresqlInstalled := true - err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql) - if err != nil { - postgresqlInstalled = false - } - var databaseCount int64 - if mysqlInstalled { - status, err := systemctl.Status("mysqld") - if status && err == nil { - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - type database struct { - Name string `json:"name"` - } - - db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/") - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("获取数据库列表失败") - databaseCount = -1 - } else { - defer db.Close() - rows, err := db.Query("SHOW DATABASES") - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("获取数据库列表失败") - databaseCount = -1 - } else { - defer rows.Close() - var databases []database - for rows.Next() { - var d database - err := rows.Scan(&d.Name) - if err != nil { - continue - } - if d.Name == "information_schema" || d.Name == "performance_schema" || d.Name == "mysql" || d.Name == "sys" { - continue - } - - databases = append(databases, d) - } - databaseCount = int64(len(databases)) - } - } - } - } - if postgresqlInstalled { - status, err := systemctl.Status("postgresql") - if status && err == nil { - raw, err := shell.Execf(`echo "\l" | su - postgres -c "psql"`) - if err == nil { - databases := strings.Split(raw, "\n") - if len(databases) >= 4 { - databases = databases[3 : len(databases)-1] - for _, db := range databases { - parts := strings.Split(db, "|") - if len(parts) != 9 || len(strings.TrimSpace(parts[0])) == 0 || strings.TrimSpace(parts[0]) == "template0" || strings.TrimSpace(parts[0]) == "template1" || strings.TrimSpace(parts[0]) == "postgres" { - continue - } - - databaseCount++ - } - } - } - } - } - - var ftpCount int64 - var ftpPlugin = r.plugin.GetInstalledBySlug("pureftpd") - if ftpPlugin.ID != 0 { - listRaw, err := shell.Execf("pure-pw list") - if len(listRaw) != 0 && err == nil { - listArr := strings.Split(listRaw, "\n") - ftpCount = int64(len(listArr)) - } - } - - var cronCount int64 - err = facades.Orm().Query().Model(models.Cron{}).Count(&cronCount) - if err != nil { - cronCount = -1 - } - - return h.Success(ctx, http.Json{ - "website": websiteCount, - "database": databaseCount, - "ftp": ftpCount, - "cron": cronCount, - }) -} - -// InstalledDbAndPhp 获取已安装的数据库和 PHP 版本 -func (r *InfoController) InstalledDbAndPhp(ctx http.Context) http.Response { - var php []models.Plugin - err := facades.Orm().Query().Where("slug like ?", "php%").Find(&php) - if err != nil { - return h.ErrorSystem(ctx) - } - - var mysql models.Plugin - mysqlInstalled := true - err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql) - if err != nil { - mysqlInstalled = false - } - - var postgresql models.Plugin - postgresqlInstalled := true - err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql) - if err != nil { - postgresqlInstalled = false - } - - type data struct { - Label string `json:"label"` - Value string `json:"value"` - } - var phpData []data - var dbData []data - phpData = append(phpData, data{Value: "0", Label: "不使用"}) - dbData = append(dbData, data{Value: "0", Label: "不使用"}) - for _, p := range php { - match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug) - if len(match) == 0 { - continue - } - - phpData = append(phpData, data{Value: strings.ReplaceAll(p.Slug, "php", ""), Label: r.plugin.GetBySlug(p.Slug).Name}) - } - - if mysqlInstalled { - dbData = append(dbData, data{Value: "mysql", Label: "MySQL"}) - } - if postgresqlInstalled { - dbData = append(dbData, data{Value: "postgresql", Label: "PostgreSQL"}) - } - - return h.Success(ctx, http.Json{ - "php": phpData, - "db": dbData, - }) -} - -// CheckUpdate 检查面板更新 -func (r *InfoController) CheckUpdate(ctx http.Context) http.Response { - current := facades.Config().GetString("panel.version") - latest, err := tools.GetLatestPanelVersion() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取最新版本失败") - } - - v1, err := version.NewVersion(current) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "版本号解析失败") - } - v2, err := version.NewVersion(latest.Version) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "版本号解析失败") - } - if v1.GreaterThanOrEqual(v2) { - return h.Success(ctx, http.Json{ - "update": false, - }) - } - - return h.Success(ctx, http.Json{ - "update": true, - }) -} - -// UpdateInfo 获取更新信息 -func (r *InfoController) UpdateInfo(ctx http.Context) http.Response { - current := facades.Config().GetString("panel.version") - latest, err := tools.GetLatestPanelVersion() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取最新版本失败") - } - - v1, err := version.NewVersion(current) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "版本号解析失败") - } - v2, err := version.NewVersion(latest.Version) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "版本号解析失败") - } - if v1.GreaterThanOrEqual(v2) { - return h.Error(ctx, http.StatusInternalServerError, "当前版本已是最新版本") - } - - versions, err := tools.GenerateVersions(current, latest.Version) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取更新信息失败") - } - - var versionInfo []tools.PanelInfo - for _, v := range versions { - info, err := tools.GetPanelVersion(v) - if err != nil { - continue - } - - versionInfo = append(versionInfo, info) - } - - return h.Success(ctx, versionInfo) -} - -// Update 更新面板 -func (r *InfoController) Update(ctx http.Context) http.Response { - var task models.Task - if err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task); err == nil { - return h.Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新") - } - if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil { - types.Status = types.StatusFailed - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("面板数据库异常,已终止操作:%s", err.Error())) - } - - panel, err := tools.GetLatestPanelVersion() - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("获取最新版本失败") - return h.Error(ctx, http.StatusInternalServerError, "获取最新版本失败") - } - - types.Status = types.StatusUpgrade - if err = tools.UpdatePanel(panel); err != nil { - types.Status = types.StatusFailed - facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{ - "error": err.Error(), - }).Info("更新面板失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - types.Status = types.StatusNormal - tools.RestartPanel() - return h.Success(ctx, nil) -} - -// Restart 重启面板 -func (r *InfoController) Restart(ctx http.Context) http.Response { - var task models.Task - err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task) - if err == nil { - return h.Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止重启") - } - - tools.RestartPanel() - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/plugin_controller.go b/app/http/controllers/plugin_controller.go deleted file mode 100644 index 5c6e56a4..00000000 --- a/app/http/controllers/plugin_controller.go +++ /dev/null @@ -1,207 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" -) - -type PluginController struct { - plugin internal.Plugin - task internal.Task -} - -func NewPluginController() *PluginController { - return &PluginController{ - plugin: services.NewPluginImpl(), - task: services.NewTaskImpl(), - } -} - -// List -// -// @Summary 插件列表 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/list [get] -func (r *PluginController) List(ctx http.Context) http.Response { - plugins := r.plugin.All() - installedPlugins, err := r.plugin.AllInstalled() - if err != nil { - return h.ErrorSystem(ctx) - } - - installedPluginsMap := make(map[string]models.Plugin) - - for _, p := range installedPlugins { - installedPluginsMap[p.Slug] = p - } - - type plugin struct { - Name string `json:"name"` - Description string `json:"description"` - Slug string `json:"slug"` - Version string `json:"version"` - Requires []string `json:"requires"` - Excludes []string `json:"excludes"` - Installed bool `json:"installed"` - InstalledVersion string `json:"installed_version"` - Show bool `json:"show"` - } - - var pluginArr []plugin - for _, item := range plugins { - installed, installedVersion, show := false, "", false - if _, ok := installedPluginsMap[item.Slug]; ok { - installed = true - installedVersion = installedPluginsMap[item.Slug].Version - show = installedPluginsMap[item.Slug].Show - } - pluginArr = append(pluginArr, plugin{ - Name: item.Name, - Description: item.Description, - Slug: item.Slug, - Version: item.Version, - Requires: item.Requires, - Excludes: item.Excludes, - Installed: installed, - InstalledVersion: installedVersion, - Show: show, - }) - } - - paged, total := h.Paginate(ctx, pluginArr) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Install -// -// @Summary 安装插件 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/install [post] -func (r *PluginController) Install(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - - if err := r.plugin.Install(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, "任务已提交") -} - -// Uninstall -// -// @Summary 卸载插件 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/uninstall [post] -func (r *PluginController) Uninstall(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - - if err := r.plugin.Uninstall(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, "任务已提交") -} - -// Update -// -// @Summary 更新插件 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/update [post] -func (r *PluginController) Update(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - - if err := r.plugin.Update(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, "任务已提交") -} - -// UpdateShow -// -// @Summary 更新插件首页显示状态 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Param show query bool true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/updateShow [post] -func (r *PluginController) UpdateShow(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - show := ctx.Request().InputBool("show") - - var plugin models.Plugin - if err := facades.Orm().Query().Where("slug", slug).First(&plugin); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{ - "slug": slug, - "err": err.Error(), - }).Info("获取插件失败") - return h.ErrorSystem(ctx) - } - if plugin.ID == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "插件未安装") - } - - plugin.Show = show - if err := facades.Orm().Query().Save(&plugin); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{ - "slug": slug, - "err": err.Error(), - }).Info("更新插件失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, "操作成功") -} - -// IsInstalled -// -// @Summary 检查插件是否已安装 -// @Tags 插件 -// @Produce json -// @Security BearerToken -// @Param slug query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/plugin/isInstalled [get] -func (r *PluginController) IsInstalled(ctx http.Context) http.Response { - slug := ctx.Request().Input("slug") - - plugin := r.plugin.GetInstalledBySlug(slug) - info := r.plugin.GetBySlug(slug) - if plugin.Slug != slug { - return h.Success(ctx, http.Json{ - "name": info.Name, - "installed": false, - }) - } - - return h.Success(ctx, http.Json{ - "name": info.Name, - "installed": true, - }) -} diff --git a/app/http/controllers/safe_controller.go b/app/http/controllers/safe_controller.go deleted file mode 100644 index 39e4211f..00000000 --- a/app/http/controllers/safe_controller.go +++ /dev/null @@ -1,377 +0,0 @@ -package controllers - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type SafeController struct { - ssh string -} - -func NewSafeController() *SafeController { - var ssh string - if os.IsRHEL() { - ssh = "sshd" - } else { - ssh = "ssh" - } - - return &SafeController{ - ssh: ssh, - } -} - -// GetFirewallStatus 获取防火墙状态 -func (r *SafeController) GetFirewallStatus(ctx http.Context) http.Response { - return h.Success(ctx, r.firewallStatus()) -} - -// SetFirewallStatus 设置防火墙状态 -func (r *SafeController) SetFirewallStatus(ctx http.Context) http.Response { - var err error - if ctx.Request().InputBool("status") { - if os.IsRHEL() { - err = systemctl.Start("firewalld") - if err == nil { - err = systemctl.Enable("firewalld") - } - } else { - _, err = shell.Execf("echo y | ufw enable") - if err == nil { - err = systemctl.Start("ufw") - } - if err == nil { - err = systemctl.Enable("ufw") - } - } - } else { - if os.IsRHEL() { - err = systemctl.Stop("firewalld") - if err == nil { - err = systemctl.Disable("firewalld") - } - } else { - _, err = shell.Execf("ufw disable") - if err == nil { - err = systemctl.Stop("ufw") - } - if err == nil { - err = systemctl.Disable("ufw") - } - } - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetFirewallRules 获取防火墙规则 -func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response { - if !r.firewallStatus() { - return h.Success(ctx, nil) - } - - var rules []map[string]string - if os.IsRHEL() { - out, err := shell.Execf("firewall-cmd --list-all") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out) - if len(match) == 0 { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []map[string]string{}, - }) - } - ports := strings.Split(match[1], " ") - for _, port := range ports { - rule := strings.Split(port, "/") - if len(rule) < 2 { - rules = append(rules, map[string]string{ - "port": rule[0], - "protocol": "all", - }) - } else { - rules = append(rules, map[string]string{ - "port": rule[0], - "protocol": rule[1], - }) - } - } - } else { - out, err := shell.Execf("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if len(out) == 0 { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []map[string]string{}, - }) - } - for _, port := range strings.Split(out, "\n") { - rule := strings.Split(port, "/") - if len(rule) < 2 { - rules = append(rules, map[string]string{ - "port": rule[0], - "protocol": "all", - }) - } else { - rules = append(rules, map[string]string{ - "port": rule[0], - "protocol": rule[1], - }) - } - } - } - - paged, total := h.Paginate(ctx, rules) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddFirewallRule 添加防火墙规则 -func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response { - if !r.firewallStatus() { - return h.Error(ctx, http.StatusInternalServerError, "防火墙未启动") - } - - port := ctx.Request().Input("port") - protocol := ctx.Request().Input("protocol") - if port == "" || protocol == "" || (protocol != "tcp" && protocol != "udp") { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - // 端口有 2 种写法,一种是 80-443,一种是 80 - if strings.Contains(port, "-") { - ports := strings.Split(port, "-") - startPort := cast.ToInt(ports[0]) - endPort := cast.ToInt(ports[1]) - if startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535 || startPort > endPort { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - } else { - port := cast.ToInt(port) - if port < 1 || port > 65535 { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - } - - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --remove-port=%s/%s --permanent", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --add-port=%s/%s --permanent", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - // ufw 需要替换 - 为 : 添加 - if strings.Contains(port, "-") { - port = strings.ReplaceAll(port, "-", ":") - } - if out, err := shell.Execf("ufw delete allow %s/%s", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw allow %s/%s", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - return h.Success(ctx, nil) -} - -// DeleteFirewallRule 删除防火墙规则 -func (r *SafeController) DeleteFirewallRule(ctx http.Context) http.Response { - if !r.firewallStatus() { - return h.Error(ctx, http.StatusUnprocessableEntity, "防火墙未启动") - } - - port := ctx.Request().Input("port") - protocol := ctx.Request().Input("protocol") - if port == "" || protocol == "" { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - if protocol == "all" { - protocol = "" - } else { - protocol = "/" + protocol - } - - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --remove-port=%s%s --permanent", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("ufw delete allow %s%s", port, protocol); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - return h.Success(ctx, nil) -} - -// firewallStatus 获取防火墙状态 -func (r *SafeController) firewallStatus() bool { - var running bool - if os.IsRHEL() { - running, _ = systemctl.Status("firewalld") - } else { - running, _ = systemctl.Status("ufw") - } - - return running -} - -// GetSshStatus 获取 SSH 状态 -func (r *SafeController) GetSshStatus(ctx http.Context) http.Response { - running, err := systemctl.Status(r.ssh) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, running) -} - -// SetSshStatus 设置 SSH 状态 -func (r *SafeController) SetSshStatus(ctx http.Context) http.Response { - if ctx.Request().InputBool("status") { - if err := systemctl.Enable(r.ssh); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := systemctl.Start(r.ssh); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } else { - if err := systemctl.Stop(r.ssh); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := systemctl.Disable(r.ssh); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - return h.Success(ctx, nil) -} - -// GetSshPort 获取 SSH 端口 -func (r *SafeController) GetSshPort(ctx http.Context) http.Response { - out, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, cast.ToInt(out)) -} - -// SetSshPort 设置 SSH 端口 -func (r *SafeController) SetSshPort(ctx http.Context) http.Response { - port := ctx.Request().InputInt("port", 0) - if port == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - - oldPort, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, oldPort) - } - _, _ = shell.Execf("sed -i 's/#Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port) - _, _ = shell.Execf("sed -i 's/Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port) - - status, _ := systemctl.Status(r.ssh) - if status { - _ = systemctl.Restart(r.ssh) - } - - return h.Success(ctx, nil) -} - -// GetPingStatus 获取 Ping 状态 -func (r *SafeController) GetPingStatus(ctx http.Context) http.Response { - if os.IsRHEL() { - out, err := shell.Execf(`firewall-cmd --list-all`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if !strings.Contains(out, `rule protocol value="icmp" drop`) { - return h.Success(ctx, true) - } else { - return h.Success(ctx, false) - } - } else { - config, err := io.Read("/etc/ufw/before.rules") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") { - return h.Success(ctx, true) - } else { - return h.Success(ctx, false) - } - } -} - -// SetPingStatus 设置 Ping 状态 -func (r *SafeController) SetPingStatus(ctx http.Context) http.Response { - var out string - var err error - if os.IsRHEL() { - if ctx.Request().InputBool("status") { - out, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`) - } else { - out, err = shell.Execf(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`) - } - } else { - if ctx.Request().InputBool("status") { - out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`) - } else { - out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`) - } - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if os.IsRHEL() { - out, err = shell.Execf(`firewall-cmd --reload`) - } else { - out, err = shell.Execf(`ufw reload`) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/setting_controller.go b/app/http/controllers/setting_controller.go deleted file mode 100644 index 4d9f5d54..00000000 --- a/app/http/controllers/setting_controller.go +++ /dev/null @@ -1,290 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" - "github.com/spf13/cast" - - requests "github.com/TheTNB/panel/v2/app/http/requests/setting" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/cert" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/tools" -) - -type SettingController struct { - setting internal.Setting -} - -func NewSettingController() *SettingController { - return &SettingController{ - setting: services.NewSettingImpl(), - } -} - -// List -// -// @Summary 设置列表 -// @Tags 面板设置 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/list [get] -func (r *SettingController) List(ctx http.Context) http.Response { - var settings []models.Setting - err := facades.Orm().Query().Get(&settings) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板设置列表失败") - return h.ErrorSystem(ctx) - } - - userID := cast.ToUint(ctx.Value("user_id")) - var user models.User - if err = facades.Orm().Query().Where("id", userID).Get(&user); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取用户信息失败") - } - - port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板端口失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "name": r.setting.Get(models.SettingKeyName), - "language": facades.Config().GetString("app.locale"), - "entrance": facades.Config().GetString("panel.entrance"), - "ssl": facades.Config().GetBool("panel.ssl"), - "website_path": r.setting.Get(models.SettingKeyWebsitePath), - "backup_path": r.setting.Get(models.SettingKeyBackupPath), - "username": user.Username, - "password": "", - "email": user.Email, - "port": port, - }) -} - -// Update -// -// @Summary 更新设置 -// @Tags 面板设置 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Update true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/update [post] -func (r *SettingController) Update(ctx http.Context) http.Response { - var updateRequest requests.Update - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - err := r.setting.Set(models.SettingKeyName, updateRequest.Name) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("保存面板名称失败") - return h.ErrorSystem(ctx) - } - - if !io.Exists(updateRequest.BackupPath) { - if err = io.Mkdir(updateRequest.BackupPath, 0644); err != nil { - return h.ErrorSystem(ctx) - } - } - err = r.setting.Set(models.SettingKeyBackupPath, updateRequest.BackupPath) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("保存备份目录失败") - return h.ErrorSystem(ctx) - } - if !io.Exists(updateRequest.WebsitePath) { - if err = io.Mkdir(updateRequest.WebsitePath, 0755); err != nil { - return h.ErrorSystem(ctx) - } - if err = io.Chown(updateRequest.WebsitePath, "www", "www"); err != nil { - return h.ErrorSystem(ctx) - } - } - err = r.setting.Set(models.SettingKeyWebsitePath, updateRequest.WebsitePath) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("保存建站目录失败") - return h.ErrorSystem(ctx) - } - - userID := cast.ToUint(ctx.Value("user_id")) - var user models.User - if err = facades.Orm().Query().Where("id", userID).Get(&user); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取用户信息失败") - } - - user.Username = updateRequest.UserName - user.Email = updateRequest.Email - if len(updateRequest.Password) > 0 { - hash, err := facades.Hash().Make(updateRequest.Password) - if err != nil { - return h.ErrorSystem(ctx) - } - user.Password = hash - } - if err = facades.Orm().Query().Save(&user); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("保存用户信息失败") - return h.ErrorSystem(ctx) - } - - oldPort, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板端口失败") - return h.ErrorSystem(ctx) - } - - port := cast.ToString(updateRequest.Port) - if oldPort != port { - if out, err := shell.Execf("sed -i 's/APP_PORT=%s/APP_PORT=%s/g' /www/panel/panel.conf", oldPort, port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --remove-port=%s/tcp --permanent", oldPort); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --add-port=%s/tcp --permanent", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("ufw delete allow %s/tcp", oldPort); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw allow %s/tcp", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - } - - oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板入口失败") - return h.ErrorSystem(ctx) - } - entrance := cast.ToString(updateRequest.Entrance) - if oldEntrance != entrance { - if out, err := shell.Execf("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=" + entrance + "!g' /www/panel/panel.conf"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - oldLanguage, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_LOCALE | awk -F '=' '{print $2}' | tr -d '\n'`) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{ - "error": err.Error(), - }).Info("获取面板语言失败") - return h.ErrorSystem(ctx) - } - if oldLanguage != updateRequest.Language { - if out, err := shell.Execf("sed -i 's/APP_LOCALE=" + oldLanguage + "/APP_LOCALE=" + updateRequest.Language + "/g' /www/panel/panel.conf"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if oldPort != port || oldEntrance != entrance || oldLanguage != updateRequest.Language { - tools.RestartPanel() - } - - return h.Success(ctx, nil) -} - -// GetHttps -// -// @Summary 获取面板 HTTPS 设置 -// @Tags 面板设置 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/https [get] -func (r *SettingController) GetHttps(ctx http.Context) http.Response { - certPath := facades.Config().GetString("http.tls.ssl.cert") - keyPath := facades.Config().GetString("http.tls.ssl.key") - crt, err := io.Read(certPath) - if err != nil { - return h.ErrorSystem(ctx) - } - key, err := io.Read(keyPath) - if err != nil { - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "https": facades.Config().GetBool("panel.ssl"), - "cert": crt, - "key": key, - }) -} - -// UpdateHttps -// -// @Summary 更新面板 HTTPS 设置 -// @Tags 面板设置 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Https true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/setting/https [post] -func (r *SettingController) UpdateHttps(ctx http.Context) http.Response { - var httpsRequest requests.Https - sanitize := h.SanitizeRequest(ctx, &httpsRequest) - if sanitize != nil { - return sanitize - } - - if httpsRequest.Https { - if _, err := cert.ParseCert(httpsRequest.Cert); err != nil { - return h.Error(ctx, http.StatusBadRequest, "证书格式错误") - } - if _, err := cert.ParseKey(httpsRequest.Key); err != nil { - return h.Error(ctx, http.StatusBadRequest, "密钥格式错误") - } - if err := io.Write(path.Executable("storage/ssl.crt"), httpsRequest.Cert, 0700); err != nil { - return h.ErrorSystem(ctx) - } - if err := io.Write(path.Executable("storage/ssl.key"), httpsRequest.Key, 0700); err != nil { - return h.ErrorSystem(ctx) - } - if out, err := shell.Execf("sed -i 's/APP_SSL=false/APP_SSL=true/g' /www/panel/panel.conf"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("sed -i 's/APP_SSL=true/APP_SSL=false/g' /www/panel/panel.conf"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - tools.RestartPanel() - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/ssh_controller.go b/app/http/controllers/ssh_controller.go deleted file mode 100644 index 06122147..00000000 --- a/app/http/controllers/ssh_controller.go +++ /dev/null @@ -1,157 +0,0 @@ -package controllers - -import ( - "bytes" - "context" - nethttp "net/http" - "sync" - "time" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/gorilla/websocket" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/ssh" -) - -type SshController struct { - AuthMethod ssh.AuthMethod - setting internal.Setting -} - -func NewSshController() *SshController { - return &SshController{ - AuthMethod: ssh.PASSWORD, - setting: services.NewSettingImpl(), - } -} - -// GetInfo 获取 SSH 配置 -func (r *SshController) GetInfo(ctx http.Context) http.Response { - host := r.setting.Get(models.SettingKeySshHost) - port := r.setting.Get(models.SettingKeySshPort) - user := r.setting.Get(models.SettingKeySshUser) - password := r.setting.Get(models.SettingKeySshPassword) - if len(host) == 0 || len(user) == 0 || len(password) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "SSH 配置不完整") - } - - return h.Success(ctx, http.Json{ - "host": host, - "port": cast.ToInt(port), - "user": user, - "password": password, - }) -} - -// UpdateInfo 更新 SSH 配置 -func (r *SshController) UpdateInfo(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "host": "required", - "port": "required", - "user": "required", - "password": "required", - }); sanitize != nil { - return sanitize - } - - host := ctx.Request().Input("host") - port := ctx.Request().Input("port") - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - if err := r.setting.Set(models.SettingKeySshHost, host); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := r.setting.Set(models.SettingKeySshPort, port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := r.setting.Set(models.SettingKeySshUser, user); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err := r.setting.Set(models.SettingKeySshPassword, password); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Session SSH 会话 -func (r *SshController) Session(ctx http.Context) http.Response { - upGrader := websocket.Upgrader{ - ReadBufferSize: 4096, - WriteBufferSize: 4096, - CheckOrigin: func(r *nethttp.Request) bool { - return true - }, - Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")}, - } - - ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil) - if err != nil { - facades.Log().Tags("面板", "SSH").With(map[string]any{ - "error": err.Error(), - }).Infof("建立连接失败") - return h.ErrorSystem(ctx) - } - defer ws.Close() - - config := ssh.ClientConfigPassword( - r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort), - r.setting.Get(models.SettingKeySshUser), - r.setting.Get(models.SettingKeySshPassword), - ) - client, err := ssh.NewSSHClient(config) - - if err != nil { - _ = ws.WriteControl(websocket.CloseMessage, - []byte(err.Error()), time.Now().Add(time.Second)) - return h.ErrorSystem(ctx) - } - defer client.Close() - - turn, err := ssh.NewTurn(ws, client) - if err != nil { - _ = ws.WriteControl(websocket.CloseMessage, - []byte(err.Error()), time.Now().Add(time.Second)) - return h.ErrorSystem(ctx) - } - defer turn.Close() - - var bufPool = sync.Pool{ - New: func() any { - return new(bytes.Buffer) - }, - } - var logBuff = bufPool.Get().(*bytes.Buffer) - logBuff.Reset() - defer bufPool.Put(logBuff) - - sshCtx, cancel := context.WithCancel(context.Background()) - wg := sync.WaitGroup{} - wg.Add(2) - go func() { - defer wg.Done() - if err = turn.LoopRead(logBuff, sshCtx); err != nil { - facades.Log().Tags("面板", "SSH").With(map[string]any{ - "error": err.Error(), - }).Infof("读取数据失败") - } - }() - go func() { - defer wg.Done() - if err = turn.SessionWait(); err != nil { - facades.Log().Tags("面板", "SSH").With(map[string]any{ - "error": err.Error(), - }).Infof("会话错误") - } - cancel() - }() - wg.Wait() - - return nil -} diff --git a/app/http/controllers/swagger_controller.go b/app/http/controllers/swagger_controller.go deleted file mode 100644 index 917ace38..00000000 --- a/app/http/controllers/swagger_controller.go +++ /dev/null @@ -1,31 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/swaggo/http-swagger/v2" - - _ "github.com/TheTNB/panel/v2/docs" -) - -type SwaggerController struct { - // Dependent services -} - -func NewSwaggerController() *SwaggerController { - return &SwaggerController{} -} - -// Index -// -// @Summary Swagger UI -// @Description Swagger UI -// @Tags Swagger -// @Success 200 -// @Failure 500 -// @Router /swagger [get] -func (r *SwaggerController) Index(ctx http.Context) http.Response { - handler := httpSwagger.Handler() - handler(ctx.Response().Writer(), ctx.Request().Origin()) - - return nil -} diff --git a/app/http/controllers/system_controller.go b/app/http/controllers/system_controller.go deleted file mode 100644 index 61dbe95c..00000000 --- a/app/http/controllers/system_controller.go +++ /dev/null @@ -1,211 +0,0 @@ -package controllers - -import ( - "fmt" - - "github.com/goravel/framework/contracts/http" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type SystemController struct { -} - -func NewSystemController() *SystemController { - return &SystemController{} -} - -// ServiceStatus -// -// @Summary 服务状态 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/status [get] -func (r *SystemController) ServiceStatus(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Query("service") - status, err := systemctl.Status(service) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", service)) - } - - return h.Success(ctx, status) -} - -// ServiceIsEnabled -// -// @Summary 是否启用服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data query string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/isEnabled [get] -func (r *SystemController) ServiceIsEnabled(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Query("service") - enabled, err := systemctl.IsEnabled(service) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务启用状态失败", service)) - } - - return h.Success(ctx, enabled) -} - -// ServiceEnable -// -// @Summary 启用服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/enable [post] -func (r *SystemController) ServiceEnable(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Enable(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启用 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceDisable -// -// @Summary 禁用服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/disable [post] -func (r *SystemController) ServiceDisable(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Disable(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("禁用 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceRestart -// -// @Summary 重启服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/restart [post] -func (r *SystemController) ServiceRestart(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Restart(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceReload -// -// @Summary 重载服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/reload [post] -func (r *SystemController) ServiceReload(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Reload(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceStart -// -// @Summary 启动服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/start [post] -func (r *SystemController) ServiceStart(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Start(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启动 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} - -// ServiceStop -// -// @Summary 停止服务 -// @Tags 系统 -// @Produce json -// @Security BearerToken -// @Param data body string true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /panel/system/service/stop [post] -func (r *SystemController) ServiceStop(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "service": "required|string", - }); sanitize != nil { - return sanitize - } - - service := ctx.Request().Input("service") - if err := systemctl.Stop(service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("停止 %s 服务失败", service)) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/task_controller.go b/app/http/controllers/task_controller.go deleted file mode 100644 index 6974f837..00000000 --- a/app/http/controllers/task_controller.go +++ /dev/null @@ -1,88 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/shell" -) - -type TaskController struct { - // Dependent services -} - -func NewTaskController() *TaskController { - return &TaskController{ - // Inject services - } -} - -// Status 获取当前任务状态 -func (r *TaskController) Status(ctx http.Context) http.Response { - var task models.Task - err := facades.Orm().Query().Where("status", models.TaskStatusWaiting).OrWhere("status", models.TaskStatusRunning).FirstOrFail(&task) - if err == nil { - return h.Success(ctx, http.Json{ - "task": true, - }) - } - - return h.Success(ctx, http.Json{ - "task": false, - }) -} - -// List 获取任务列表 -func (r *TaskController) List(ctx http.Context) http.Response { - var tasks []models.Task - var total int64 - err := facades.Orm().Query().Order("id desc").Paginate(ctx.Request().QueryInt("page", 1), ctx.Request().QueryInt("limit", 10), &tasks, &total) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{ - "error": err.Error(), - }).Info("查询任务列表失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": tasks, - }) -} - -// Log 获取任务日志 -func (r *TaskController) Log(ctx http.Context) http.Response { - var task models.Task - err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).FirstOrFail(&task) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{ - "id": ctx.Request().QueryInt("id"), - "error": err.Error(), - }).Info("查询任务失败") - return h.ErrorSystem(ctx) - } - - log, err := shell.Execf(`tail -n 500 '` + task.Log + `'`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "日志已被清理") - } - - return h.Success(ctx, log) -} - -// Delete 删除任务 -func (r *TaskController) Delete(ctx http.Context) http.Response { - var task models.Task - _, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "任务中心").With(map[string]any{ - "id": ctx.Request().QueryInt("id"), - "error": err.Error(), - }).Info("删除任务失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/controllers/user_controller.go b/app/http/controllers/user_controller.go deleted file mode 100644 index d9a6830a..00000000 --- a/app/http/controllers/user_controller.go +++ /dev/null @@ -1,123 +0,0 @@ -package controllers - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/http/requests/user" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/h" -) - -type UserController struct { - // Dependent services -} - -func NewUserController() *UserController { - return &UserController{ - // Inject services - } -} - -// Login -// -// @Summary 登录 -// @Tags 用户鉴权 -// @Accept json -// @Produce json -// @Param data body requests.Login true "request" -// @Success 200 {object} SuccessResponse -// @Failure 403 {object} ErrorResponse "用户名或密码错误" -// @Failure 500 {object} ErrorResponse "系统内部错误 -// @Router /panel/user/login [post] -func (r *UserController) Login(ctx http.Context) http.Response { - var loginRequest requests.Login - sanitize := h.SanitizeRequest(ctx, &loginRequest) - if sanitize != nil { - return sanitize - } - - var user models.User - err := facades.Orm().Query().Where("username", loginRequest.Username).First(&user) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{ - "error": err.Error(), - }).Info("查询用户失败") - return h.ErrorSystem(ctx) - } - - if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) { - return h.Error(ctx, http.StatusForbidden, "用户名或密码错误") - } - - if facades.Hash().NeedsRehash(user.Password) { - user.Password, err = facades.Hash().Make(loginRequest.Password) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{ - "error": err.Error(), - }).Info("更新密码失败") - return h.ErrorSystem(ctx) - } - } - - ctx.Request().Session().Put("user_id", user.ID) - return h.Success(ctx, nil) -} - -// Logout -// -// @Summary 登出 -// @Tags 用户鉴权 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/user/logout [post] -func (r *UserController) Logout(ctx http.Context) http.Response { - if ctx.Request().HasSession() { - ctx.Request().Session().Forget("user_id") - } - - return h.Success(ctx, nil) -} - -// IsLogin -// -// @Summary 是否登录 -// @Tags 用户鉴权 -// @Produce json -// @Success 200 {object} SuccessResponse -// @Router /panel/user/isLogin [get] -func (r *UserController) IsLogin(ctx http.Context) http.Response { - if !ctx.Request().HasSession() { - return h.Success(ctx, false) - } - - return h.Success(ctx, ctx.Request().Session().Has("user_id")) -} - -// Info -// -// @Summary 用户信息 -// @Tags 用户鉴权 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse -// @Router /panel/user/info [get] -func (r *UserController) Info(ctx http.Context) http.Response { - userID := cast.ToUint(ctx.Value("user_id")) - var user models.User - if err := facades.Orm().Query().Where("id", userID).Get(&user); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{ - "error": err.Error(), - }).Info("获取用户信息失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, http.Json{ - "id": user.ID, - "role": []string{"admin"}, - "username": user.Username, - "email": user.Email, - }) -} diff --git a/app/http/controllers/website_controller.go b/app/http/controllers/website_controller.go deleted file mode 100644 index 6bfb8e45..00000000 --- a/app/http/controllers/website_controller.go +++ /dev/null @@ -1,646 +0,0 @@ -package controllers - -import ( - "fmt" - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - commonrequests "github.com/TheTNB/panel/v2/app/http/requests/common" - requests "github.com/TheTNB/panel/v2/app/http/requests/website" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type WebsiteController struct { - website internal.Website - setting internal.Setting - backup internal.Backup -} - -func NewWebsiteController() *WebsiteController { - return &WebsiteController{ - website: services.NewWebsiteImpl(), - setting: services.NewSettingImpl(), - backup: services.NewBackupImpl(), - } -} - -// List -// -// @Summary 获取网站列表 -// @Tags 网站 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites [get] -func (r *WebsiteController) List(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - total, websites, err := r.website.List(paginateRequest.Page, paginateRequest.Limit) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取网站列表失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, http.Json{ - "total": total, - "items": websites, - }) -} - -// Add -// -// @Summary 添加网站 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Add true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites [post] -func (r *WebsiteController) Add(ctx http.Context) http.Response { - var addRequest requests.Add - sanitize := h.SanitizeRequest(ctx, &addRequest) - if sanitize != nil { - return sanitize - } - - if len(addRequest.Path) == 0 { - addRequest.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + addRequest.Name - } - - _, err := r.website.Add(addRequest) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("添加网站失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Delete -// -// @Summary 删除网站 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.Delete true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/delete [post] -func (r *WebsiteController) Delete(ctx http.Context) http.Response { - var deleteRequest requests.Delete - sanitize := h.SanitizeRequest(ctx, &deleteRequest) - if sanitize != nil { - return sanitize - } - - if err := r.website.Delete(deleteRequest); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": deleteRequest.ID, - "error": err.Error(), - }).Info("删除网站失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetDefaultConfig -// -// @Summary 获取默认配置 -// @Tags 网站 -// @Produce json -// @Security BearerToken -// @Success 200 {object} SuccessResponse{data=map[string]string} -// @Router /panel/website/defaultConfig [get] -func (r *WebsiteController) GetDefaultConfig(ctx http.Context) http.Response { - index, err := io.Read("/www/server/openresty/html/index.html") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - stop, err := io.Read("/www/server/openresty/html/stop.html") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, http.Json{ - "index": index, - "stop": stop, - }) -} - -// SaveDefaultConfig -// -// @Summary 保存默认配置 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body map[string]string true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/website/defaultConfig [post] -func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response { - index := ctx.Request().Input("index") - stop := ctx.Request().Input("stop") - - if err := io.Write("/www/server/openresty/html/index.html", index, 0644); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("保存默认首页配置失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := io.Write("/www/server/openresty/html/stop.html", stop, 0644); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("保存默认停止页配置失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetConfig -// -// @Summary 获取网站配置 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse{data=types.WebsiteAdd} -// @Router /panel/websites/{id}/config [get] -func (r *WebsiteController) GetConfig(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - config, err := r.website.GetConfig(idRequest.ID) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("获取网站配置失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveConfig -// -// @Summary 保存网站配置 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Param data body requests.SaveConfig true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/config [post] -func (r *WebsiteController) SaveConfig(ctx http.Context) http.Response { - var saveConfigRequest requests.SaveConfig - sanitize := h.SanitizeRequest(ctx, &saveConfigRequest) - if sanitize != nil { - return sanitize - } - - err := r.website.SaveConfig(saveConfigRequest) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ClearLog -// -// @Summary 清空网站日志 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/log [delete] -func (r *WebsiteController) ClearLog(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website) - if err != nil { - return h.ErrorSystem(ctx) - } - - if err := io.Remove("/www/wwwlogs/" + website.Name + ".log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UpdateRemark -// -// @Summary 更新网站备注 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/updateRemark [post] -func (r *WebsiteController) UpdateRemark(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website) - if err != nil { - return h.ErrorSystem(ctx) - } - - website.Remark = ctx.Request().Input("remark") - if err = facades.Orm().Query().Save(&website); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("更新网站备注失败") - return h.ErrorSystem(ctx) - } - - return h.Success(ctx, nil) -} - -// BackupList -// -// @Summary 获取网站备份列表 -// @Tags 网站 -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} SuccessResponse{data=[]types.BackupFile} -// @Router /panel/website/backupList [get] -func (r *WebsiteController) BackupList(ctx http.Context) http.Response { - var paginateRequest commonrequests.Paginate - sanitize := h.SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return sanitize - } - - backups, err := r.backup.WebsiteList() - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("获取备份列表失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, backups) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// CreateBackup -// -// @Summary 创建网站备份 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/createBackup [post] -func (r *WebsiteController) CreateBackup(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("获取网站信息失败") - return h.ErrorSystem(ctx) - } - - if err := r.backup.WebSiteBackup(website); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("备份网站失败") - return h.Error(ctx, http.StatusInternalServerError, "备份网站失败: "+err.Error()) - } - - return h.Success(ctx, nil) -} - -// UploadBackup -// -// @Summary 上传网站备份 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param file formData file true "备份文件" -// @Success 200 {object} SuccessResponse -// @Router /panel/website/uploadBackup [put] -func (r *WebsiteController) UploadBackup(ctx http.Context) http.Response { - file, err := ctx.Request().File("file") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "上传文件失败") - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website" - if !io.Exists(backupPath) { - if err = io.Mkdir(backupPath, 0644); err != nil { - return nil - } - } - - name := file.GetClientOriginalName() - _, err = file.StoreAs(backupPath, name) - if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "error": err.Error(), - }).Info("上传备份失败") - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// RestoreBackup -// -// @Summary 还原网站备份 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/restoreBackup [post] -func (r *WebsiteController) RestoreBackup(ctx http.Context) http.Response { - var restoreBackupRequest requests.RestoreBackup - sanitize := h.SanitizeRequest(ctx, &restoreBackupRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - if err := facades.Orm().Query().Where("id", restoreBackupRequest.ID).Get(&website); err != nil { - return h.ErrorSystem(ctx) - } - - if err := r.backup.WebsiteRestore(website, restoreBackupRequest.Name); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": restoreBackupRequest.ID, - "file": restoreBackupRequest.Name, - "error": err.Error(), - }).Info("还原网站失败") - return h.Error(ctx, http.StatusInternalServerError, "还原网站失败: "+err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteBackup -// -// @Summary 删除网站备份 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param data body requests.DeleteBackup true "request" -// @Success 200 {object} SuccessResponse -// @Router /panel/website/deleteBackup [delete] -func (r *WebsiteController) DeleteBackup(ctx http.Context) http.Response { - var deleteBackupRequest requests.DeleteBackup - sanitize := h.SanitizeRequest(ctx, &deleteBackupRequest) - if sanitize != nil { - return sanitize - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return nil - } - } - - if err := io.Remove(backupPath + "/" + deleteBackupRequest.Name); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ResetConfig -// -// @Summary 重置网站配置 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/resetConfig [post] -func (r *WebsiteController) ResetConfig(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil { - return h.ErrorSystem(ctx) - } - - website.Status = true - website.SSL = false - if err := facades.Orm().Query().Save(&website); err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{ - "id": idRequest.ID, - "error": err.Error(), - }).Info("保存网站配置失败") - return h.ErrorSystem(ctx) - } - - raw := fmt.Sprintf(` -# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! -# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 -server -{ - # port标记位开始 - listen 80; - # port标记位结束 - # server_name标记位开始 - server_name localhost; - # server_name标记位结束 - # index标记位开始 - index index.php index.html; - # index标记位结束 - # root标记位开始 - root %s; - # root标记位结束 - - # ssl标记位开始 - # ssl标记位结束 - - # php标记位开始 - include enable-php-%d.conf; - # php标记位结束 - - # waf标记位开始 - waf off; - waf_rule_path /www/server/openresty/ngx_waf/assets/rules/; - waf_mode DYNAMIC; - waf_cc_deny rate=1000r/m duration=60m; - waf_cache capacity=50; - # waf标记位结束 - - # 错误页配置,可自行设置 - error_page 404 /404.html; - #error_page 502 /502.html; - - # acme证书签发配置,不可修改 - include /www/server/vhost/acme/%s.conf; - - # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 - include /www/server/vhost/rewrite/%s.conf; - - # 面板默认禁止访问部分敏感目录,可自行修改 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) - { - return 404; - } - # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 - location ~ .*\.(js|css)$ - { - expires 1h; - error_log /dev/null; - access_log /dev/null; - } - - access_log /www/wwwlogs/%s.log; - error_log /www/wwwlogs/%s.log; -} - -`, website.Path, website.PHP, website.Name, website.Name, website.Name, website.Name) - if err := io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil { - return nil - } - if err := io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644); err != nil { - return nil - } - if err := io.Write("/www/server/vhost/acme/"+website.Name+".conf", "", 0644); err != nil { - return nil - } - if err := systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) - } - - return h.Success(ctx, nil) -} - -// Status -// -// @Summary 获取网站状态 -// @Tags 网站 -// @Accept json -// @Produce json -// @Security BearerToken -// @Param id path int true "网站 ID" -// @Success 200 {object} SuccessResponse -// @Router /panel/websites/{id}/status [post] -func (r *WebsiteController) Status(ctx http.Context) http.Response { - var idRequest requests.ID - sanitize := h.SanitizeRequest(ctx, &idRequest) - if sanitize != nil { - return sanitize - } - - website := models.Website{} - if err := facades.Orm().Query().Where("id", idRequest.ID).Get(&website); err != nil { - return h.ErrorSystem(ctx) - } - - website.Status = ctx.Request().InputBool("status") - if err := facades.Orm().Query().Save(&website); err != nil { - return h.ErrorSystem(ctx) - } - - raw, err := io.Read("/www/server/vhost/" + website.Name + ".conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - // 运行目录 - rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束") - match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig) - if len(match) == 2 { - if website.Status { - root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig) - raw = strings.ReplaceAll(raw, rootConfig, " root "+root[1]+";\n ") - } else { - raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ") - } - } - - // 默认文件 - indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束") - match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig) - if len(match) == 2 { - if website.Status { - index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig) - raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ") - } else { - raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ") - } - } - - if err = io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) - } - - return h.Success(ctx, nil) -} diff --git a/app/http/kernel.go b/app/http/kernel.go deleted file mode 100644 index 8942e963..00000000 --- a/app/http/kernel.go +++ /dev/null @@ -1,23 +0,0 @@ -package http - -import ( - "github.com/goravel/framework/contracts/http" - sessionmiddleware "github.com/goravel/framework/session/middleware" - - "github.com/TheTNB/panel/v2/app/http/middleware" -) - -type Kernel struct { -} - -// The application's global HTTP middleware stack. -// These middleware are run during every request to your application. -func (kernel Kernel) Middleware() []http.Middleware { - return []http.Middleware{ - sessionmiddleware.StartSession(), - middleware.Log(), - middleware.Status(), - middleware.Entrance(), - middleware.Static(), - } -} diff --git a/app/http/middleware/entrance.go b/app/http/middleware/entrance.go deleted file mode 100644 index 67188ea5..00000000 --- a/app/http/middleware/entrance.go +++ /dev/null @@ -1,39 +0,0 @@ -package middleware - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" -) - -func Entrance() http.Middleware { - return func(ctx http.Context) { - translate := facades.Lang(ctx) - - if !ctx.Request().HasSession() { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ - "message": translate.Get("auth.session.missing"), - }) - return - } - - entrance := facades.Config().GetString("panel.entrance") - if ctx.Request().Path() == entrance { - ctx.Request().Session().Put("verify_entrance", true) - _ = ctx.Response().Redirect(http.StatusFound, "/login").Render() - ctx.Request().AbortWithStatus(http.StatusFound) - return - } - - if !facades.Config().GetBool("app.debug") && - (ctx.Request().Session().Missing("verify_entrance") || !cast.ToBool(ctx.Request().Session().Get("verify_entrance"))) && - ctx.Request().Path() != "/robots.txt" { - ctx.Request().AbortWithStatusJson(http.StatusTeapot, http.Json{ - "message": "请通过正确的入口访问", - }) - return - } - - ctx.Request().Next() - } -} diff --git a/app/http/middleware/log.go b/app/http/middleware/log.go deleted file mode 100644 index 4dbc829d..00000000 --- a/app/http/middleware/log.go +++ /dev/null @@ -1,20 +0,0 @@ -package middleware - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" -) - -// Log 记录请求日志 -func Log() http.Middleware { - return func(ctx http.Context) { - facades.Log().Channel("http").With(map[string]any{ - "Method": ctx.Request().Method(), - "URL": ctx.Request().FullUrl(), - "IP": ctx.Request().Ip(), - "UA": ctx.Request().Header("User-Agent"), - "Body": ctx.Request().All(), - }).Info("HTTP Request") - ctx.Request().Next() - } -} diff --git a/app/http/middleware/must_install.go b/app/http/middleware/must_install.go deleted file mode 100644 index 6b4c5664..00000000 --- a/app/http/middleware/must_install.go +++ /dev/null @@ -1,93 +0,0 @@ -package middleware - -import ( - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/translation" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/internal/services" -) - -// MustInstall 确保已安装插件 -func MustInstall() http.Middleware { - return func(ctx http.Context) { - path := ctx.Request().Path() - translate := facades.Lang(ctx) - var slug string - if strings.HasPrefix(path, "/api/panel/website") { - slug = "openresty" - } else if strings.HasPrefix(path, "/api/panel/container") { - slug = "podman" - } else { - pathArr := strings.Split(path, "/") - if len(pathArr) < 4 { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("errors.plugin.notExist"), - }) - return - } - slug = pathArr[3] - } - - plugin := services.NewPluginImpl().GetBySlug(slug) - installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug) - installedPlugins, err := services.NewPluginImpl().AllInstalled() - if err != nil { - ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{ - "message": translate.Get("errors.internal"), - }) - return - } - - if installedPlugin.Slug != plugin.Slug { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("errors.plugin.notInstalled", translation.Option{ - Replace: map[string]string{ - "slug": slug, - }, - }), - }) - return - } - - pluginsMap := make(map[string]bool) - - for _, p := range installedPlugins { - pluginsMap[p.Slug] = true - } - - for _, require := range plugin.Requires { - _, requireFound := pluginsMap[require] - if !requireFound { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("errors.plugin.dependent", translation.Option{ - Replace: map[string]string{ - "slug": slug, - "dependency": require, - }, - }), - }) - return - } - } - - for _, exclude := range plugin.Excludes { - _, excludeFound := pluginsMap[exclude] - if excludeFound { - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("errors.plugin.incompatible", translation.Option{ - Replace: map[string]string{ - "slug": slug, - "exclude": exclude, - }, - }), - }) - return - } - } - - ctx.Request().Next() - } -} diff --git a/app/http/middleware/session.go b/app/http/middleware/session.go deleted file mode 100644 index c0a775df..00000000 --- a/app/http/middleware/session.go +++ /dev/null @@ -1,53 +0,0 @@ -package middleware - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" -) - -// Session 确保通过 JWT 鉴权 -func Session() http.Middleware { - return func(ctx http.Context) { - translate := facades.Lang(ctx) - - if !ctx.Request().HasSession() { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ - "message": translate.Get("auth.session.missing"), - }) - return - } - - if ctx.Request().Session().Missing("user_id") { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ - "message": translate.Get("auth.session.expired"), - }) - return - } - - userID := cast.ToUint(ctx.Request().Session().Get("user_id")) - if userID == 0 { - ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{ - "message": translate.Get("auth.session.invalid"), - }) - return - } - - // 刷新会话 - /*if err := ctx.Request().Session().Regenerate(); err == nil { - ctx.Response().Cookie(http.Cookie{ - Name: ctx.Request().Session().GetName(), - Value: ctx.Request().Session().GetID(), - MaxAge: facades.Config().GetInt("session.lifetime") * 60, - Path: facades.Config().GetString("session.path"), - Domain: facades.Config().GetString("session.domain"), - Secure: facades.Config().GetBool("session.secure"), - HttpOnly: facades.Config().GetBool("session.http_only"), - SameSite: facades.Config().GetString("session.same_site"), - }) - }*/ - - ctx.WithValue("user_id", userID) - ctx.Request().Next() - } -} diff --git a/app/http/middleware/static.go b/app/http/middleware/static.go deleted file mode 100644 index 2a92cfd8..00000000 --- a/app/http/middleware/static.go +++ /dev/null @@ -1,16 +0,0 @@ -package middleware - -import ( - "github.com/gin-contrib/static" - "github.com/goravel/framework/contracts/http" - "github.com/goravel/gin" - - "github.com/TheTNB/panel/v2/embed" -) - -func Static() http.Middleware { - return func(ctx http.Context) { - static.Serve("/", static.EmbedFolder(embed.PublicFS, "frontend"))(ctx.(*gin.Context).Instance()) - ctx.Request().Next() - } -} diff --git a/app/http/middleware/status.go b/app/http/middleware/status.go deleted file mode 100644 index 9f128fc7..00000000 --- a/app/http/middleware/status.go +++ /dev/null @@ -1,40 +0,0 @@ -package middleware - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -// Status 检查程序状态 -func Status() http.Middleware { - return func(ctx http.Context) { - translate := facades.Lang(ctx) - switch types.Status { - case types.StatusUpgrade: - ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{ - "message": translate.Get("status.upgrade"), - }) - return - case types.StatusMaintain: - ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{ - "message": translate.Get("status.maintain"), - }) - return - case types.StatusClosed: - ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{ - "message": translate.Get("status.closed"), - }) - return - case types.StatusFailed: - ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{ - "message": translate.Get("status.failed"), - }) - return - default: - ctx.Request().Next() - return - } - } -} diff --git a/app/http/requests/cert/cert_deploy.go b/app/http/requests/cert/cert_deploy.go deleted file mode 100644 index 7be07f34..00000000 --- a/app/http/requests/cert/cert_deploy.go +++ /dev/null @@ -1,41 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type CertDeploy struct { - ID uint `form:"id" json:"id"` - WebsiteID uint `form:"website_id" json:"website_id"` -} - -func (r *CertDeploy) Authorize(ctx http.Context) error { - return nil -} - -func (r *CertDeploy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:certs,id", - "website_id": "required|uint|min:1|exists:websites,id", - } -} - -func (r *CertDeploy) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - "website_id": "uint", - } -} - -func (r *CertDeploy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertDeploy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertDeploy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/cert_show_and_destroy.go b/app/http/requests/cert/cert_show_and_destroy.go deleted file mode 100644 index 597606e7..00000000 --- a/app/http/requests/cert/cert_show_and_destroy.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type CertShowAndDestroy struct { - ID uint `form:"id" json:"id"` -} - -func (r *CertShowAndDestroy) Authorize(ctx http.Context) error { - return nil -} - -func (r *CertShowAndDestroy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:certs,id", - } -} - -func (r *CertShowAndDestroy) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertShowAndDestroy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertShowAndDestroy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/cert_store.go b/app/http/requests/cert/cert_store.go deleted file mode 100644 index 60539558..00000000 --- a/app/http/requests/cert/cert_store.go +++ /dev/null @@ -1,50 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type CertStore struct { - Type string `form:"type" json:"type"` - Domains []string `form:"domains" json:"domains"` - AutoRenew bool `form:"auto_renew" json:"auto_renew"` - UserID uint `form:"user_id" json:"user_id"` - DNSID uint `form:"dns_id" json:"dns_id"` - WebsiteID uint `form:"website_id" json:"website_id"` -} - -func (r *CertStore) Authorize(ctx http.Context) error { - return nil -} - -func (r *CertStore) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "type": "required|in:P256,P384,2048,4096", - "domains": "required|slice", - "auto_renew": "required|bool", - "user_id": "required|uint|exists:cert_users,id", - "dns_id": "uint", - "website_id": "uint", - } -} - -func (r *CertStore) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "user_id": "uint", - "dns_id": "uint", - "website_id": "uint", - } -} - -func (r *CertStore) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertStore) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertStore) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/cert_update.go b/app/http/requests/cert/cert_update.go deleted file mode 100644 index f88dfb46..00000000 --- a/app/http/requests/cert/cert_update.go +++ /dev/null @@ -1,52 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type CertUpdate struct { - ID uint `form:"id" json:"id"` - Type string `form:"type" json:"type"` - Domains []string `form:"domains" json:"domains"` - AutoRenew bool `form:"auto_renew" json:"auto_renew"` - UserID uint `form:"user_id" json:"user_id"` - DNSID uint `form:"dns_id" json:"dns_id"` - WebsiteID uint `form:"website_id" json:"website_id"` -} - -func (r *CertUpdate) Authorize(ctx http.Context) error { - return nil -} - -func (r *CertUpdate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:certs,id", - "type": "required|in:P256,P384,2048,4096", - "domains": "required|slice", - "auto_renew": "required|bool", - "user_id": "required|uint|exists:cert_users,id", - "dns_id": "uint", - "website_id": "uint", - } -} - -func (r *CertUpdate) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "user_id": "uint", - "dns_id": "uint", - "website_id": "uint", - } -} - -func (r *CertUpdate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertUpdate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *CertUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/dns_show_and_destroy.go b/app/http/requests/cert/dns_show_and_destroy.go deleted file mode 100644 index e709295b..00000000 --- a/app/http/requests/cert/dns_show_and_destroy.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type DNSShowAndDestroy struct { - ID uint `form:"id" json:"id"` -} - -func (r *DNSShowAndDestroy) Authorize(ctx http.Context) error { - return nil -} - -func (r *DNSShowAndDestroy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:cert_dns,id", - } -} - -func (r *DNSShowAndDestroy) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSShowAndDestroy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSShowAndDestroy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/dns_store.go b/app/http/requests/cert/dns_store.go deleted file mode 100644 index 6b2d0504..00000000 --- a/app/http/requests/cert/dns_store.go +++ /dev/null @@ -1,47 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/acme" -) - -type DNSStore struct { - Type string `form:"type" json:"type"` - Name string `form:"name" json:"name"` - Data acme.DNSParam `form:"data" json:"data"` -} - -func (r *DNSStore) Authorize(ctx http.Context) error { - return nil -} - -func (r *DNSStore) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "type": "required|in:dnspod,tencent,aliyun,cloudflare", - "name": "required", - "data": "required", - "data.id": "required_if:type,dnspod", - "data.token": "required_if:type,dnspod", - "data.access_key": "required_if:type,aliyun,tencent", - "data.secret_key": "required_if:type,aliyun,tencent", - "data.api_key": "required_if:type,cloudflare", - } -} - -func (r *DNSStore) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSStore) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSStore) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSStore) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/dns_update.go b/app/http/requests/cert/dns_update.go deleted file mode 100644 index 06b5e446..00000000 --- a/app/http/requests/cert/dns_update.go +++ /dev/null @@ -1,50 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/acme" -) - -type DNSUpdate struct { - ID uint `form:"id" json:"id"` - Type string `form:"type" json:"type"` - Name string `form:"name" json:"name"` - Data acme.DNSParam `form:"data" json:"data"` -} - -func (r *DNSUpdate) Authorize(ctx http.Context) error { - return nil -} - -func (r *DNSUpdate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:cert_dns,id", - "type": "required|in:dnspod,aliyun,cloudflare", - "name": "required", - "data": "required", - "data.id": "required_if:type,dnspod", - "data.token": "required_if:type,dnspod", - "data.access_key": "required_if:type,aliyun", - "data.secret_key": "required_if:type,aliyun", - "data.email": "required_if:type,cloudflare", - "data.api_key": "required_if:type,cloudflare", - } -} - -func (r *DNSUpdate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSUpdate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSUpdate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DNSUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/obtain.go b/app/http/requests/cert/obtain.go deleted file mode 100644 index 53f82889..00000000 --- a/app/http/requests/cert/obtain.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Obtain struct { - ID uint `form:"id" json:"id"` -} - -func (r *Obtain) Authorize(ctx http.Context) error { - return nil -} - -func (r *Obtain) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:certs,id", - } -} - -func (r *Obtain) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Obtain) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Obtain) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Obtain) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/renew.go b/app/http/requests/cert/renew.go deleted file mode 100644 index 39be78cd..00000000 --- a/app/http/requests/cert/renew.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Renew struct { - ID uint `form:"id" json:"id"` -} - -func (r *Renew) Authorize(ctx http.Context) error { - return nil -} - -func (r *Renew) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:certs,id", - } -} - -func (r *Renew) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Renew) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Renew) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Renew) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/user_show_and_destroy.go b/app/http/requests/cert/user_show_and_destroy.go deleted file mode 100644 index ad8cf7e9..00000000 --- a/app/http/requests/cert/user_show_and_destroy.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UserShowAndDestroy struct { - ID uint `form:"id" json:"id"` -} - -func (r *UserShowAndDestroy) Authorize(ctx http.Context) error { - return nil -} - -func (r *UserShowAndDestroy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:cert_users,id", - } -} - -func (r *UserShowAndDestroy) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserShowAndDestroy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserShowAndDestroy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserShowAndDestroy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/user_store.go b/app/http/requests/cert/user_store.go deleted file mode 100644 index ad14f1e8..00000000 --- a/app/http/requests/cert/user_store.go +++ /dev/null @@ -1,44 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UserStore struct { - CA string `form:"ca" json:"ca"` - Email string `form:"email" json:"email"` - Kid string `form:"kid" json:"kid"` - HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` - KeyType string `form:"key_type" json:"key_type"` -} - -func (r *UserStore) Authorize(ctx http.Context) error { - return nil -} - -func (r *UserStore) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass", - "email": "required|email", - "kid": "required_if:ca,sslcom,google", - "hmac_encoded": "required_if:ca,sslcom,google", - "key_type": "required|in:P256,P384,2048,4096", - } -} - -func (r *UserStore) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserStore) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserStore) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserStore) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/cert/user_update.go b/app/http/requests/cert/user_update.go deleted file mode 100644 index ccdb3d67..00000000 --- a/app/http/requests/cert/user_update.go +++ /dev/null @@ -1,46 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UserUpdate struct { - ID uint `form:"id" json:"id"` - CA string `form:"ca" json:"ca"` - Email string `form:"email" json:"email"` - Kid string `form:"kid" json:"kid"` - HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` - KeyType string `form:"key_type" json:"key_type"` -} - -func (r *UserUpdate) Authorize(ctx http.Context) error { - return nil -} - -func (r *UserUpdate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|uint|min:1|exists:cert_users,id", - "ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass", - "email": "required|email", - "kid": "required_if:ca,sslcom,google", - "hmac_encoded": "required_if:ca,sslcom,google", - "key_type": "required|in:P256,P384,2048,4096", - } -} - -func (r *UserUpdate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserUpdate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserUpdate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UserUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/common/paginate.go b/app/http/requests/common/paginate.go deleted file mode 100644 index 99f0373e..00000000 --- a/app/http/requests/common/paginate.go +++ /dev/null @@ -1,41 +0,0 @@ -package commonrequests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Paginate struct { - Page int `form:"page" json:"page"` - Limit int `form:"limit" json:"limit"` -} - -func (r *Paginate) Authorize(ctx http.Context) error { - return nil -} - -func (r *Paginate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "page": "required|int|min:1", - "limit": "required|int|min:1", - } -} - -func (r *Paginate) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "page": "int", - "limit": "int", - } -} - -func (r *Paginate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Paginate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Paginate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/container_create.go b/app/http/requests/container/container_create.go deleted file mode 100644 index 6fb2caa9..00000000 --- a/app/http/requests/container/container_create.go +++ /dev/null @@ -1,85 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -type ContainerCreate struct { - Name string `form:"name" json:"name"` - Image string `form:"image" json:"image"` - Ports []types.ContainerPort `form:"ports" json:"ports"` - Network string `form:"network" json:"network"` - Volumes []types.ContainerVolume `form:"volumes" json:"volumes"` - Labels []types.KV `form:"labels" json:"labels"` - Env []types.KV `form:"env" json:"env"` - Entrypoint []string `form:"entrypoint" json:"entrypoint"` - Command []string `form:"command" json:"command"` - RestartPolicy string `form:"restart_policy" json:"restart_policy"` - AutoRemove bool `form:"auto_remove" json:"auto_remove"` - Privileged bool `form:"privileged" json:"privileged"` - OpenStdin bool `form:"openStdin" json:"open_stdin"` - PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"` - Tty bool `form:"tty" json:"tty"` - CPUShares int64 `form:"cpu_shares" json:"cpu_shares"` - CPUs int64 `form:"cpus" json:"cpus"` - Memory int64 `form:"memory" json:"memory"` -} - -func (r *ContainerCreate) Authorize(ctx http.Context) error { - return nil -} - -func (r *ContainerCreate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - "image": "required|string", - "ports": "slice", - /*"ports.*.host": "string", - "ports.*.host_start": "int", - "ports.*.host_end": "int", - "ports.*.container_start": "int", - "ports.*.container_end": "int", - "ports.*.protocol": "string|in:tcp,udp",*/ - "network": "string", - "volumes": "slice", - /*"volumes.*.host": "string", - "volumes.*.container": "string", - "volumes.*.mode": "string|in:ro,rw",*/ - "labels": "slice", - "env": "slice", - "entrypoint": "slice", - "command": "slice", - "restart_policy": "string|in:always,on-failure,unless-stopped,no", - "auto_remove": "bool", - "privileged": "bool", - "open_stdin": "bool", - "publish_all_ports": "bool", - "tty": "bool", - "cpu_shares": "int", - "cpus": "int", - "memory": "int", - } -} - -func (r *ContainerCreate) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "cpu_shares": "int", - "cpus": "int", - "memory": "int", - } -} - -func (r *ContainerCreate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerCreate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerCreate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/container_rename.go b/app/http/requests/container/container_rename.go deleted file mode 100644 index 8b746cf0..00000000 --- a/app/http/requests/container/container_rename.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type ContainerRename struct { - ID string `form:"id" json:"id"` - Name string `form:"name" json:"name"` -} - -func (r *ContainerRename) Authorize(ctx http.Context) error { - return nil -} - -func (r *ContainerRename) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|string", - "name": "required|string", - } -} - -func (r *ContainerRename) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerRename) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerRename) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerRename) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/container_update.go b/app/http/requests/container/container_update.go deleted file mode 100644 index e7e3da9e..00000000 --- a/app/http/requests/container/container_update.go +++ /dev/null @@ -1,74 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -type ContainerUpdate struct { - ID string `form:"id" json:"id"` - Name string `form:"name" json:"name"` - Image string `form:"image" json:"image"` - Ports []types.ContainerPort `form:"ports" json:"ports"` - Network string `form:"network" json:"network"` - Volumes []types.ContainerVolume `form:"volumes" json:"volumes"` - Labels []string `form:"labels" json:"labels"` - Env []string `form:"env" json:"env"` - Entrypoint []string `form:"entrypoint" json:"entrypoint"` - Command []string `form:"command" json:"command"` - RestartPolicy string `form:"restart_policy" json:"restart_policy"` - AutoRemove bool `form:"auto_remove" json:"auto_remove"` - Privileged bool `form:"privileged" json:"privileged"` - OpenStdin bool `form:"openStdin" json:"open_stdin"` - PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"` - Tty bool `form:"tty" json:"tty"` - CPUShares int64 `form:"cpu_shares" json:"cpu_shares"` - CPUs int64 `form:"cpus" json:"cpus"` - Memory int64 `form:"memory" json:"memory"` -} - -func (r *ContainerUpdate) Authorize(ctx http.Context) error { - return nil -} - -func (r *ContainerUpdate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|string", - "name": "required|string", - "image": "required|string", - "ports": "slice", - "network": "string", - "volumes": "slice", - "labels": "slice", - "env": "slice", - "entrypoint": "slice", - "command": "slice", - "restart_policy": "string|in:always,on-failure,unless-stopped,no", - "auto_remove": "bool", - "privileged": "bool", - "open_stdin": "bool", - "publish_all_ports": "bool", - "tty": "bool", - "cpu_shares": "int", - "cpus": "int", - "memory": "int", - } -} - -func (r *ContainerUpdate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerUpdate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerUpdate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ContainerUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/id.go b/app/http/requests/container/id.go deleted file mode 100644 index 6f16469e..00000000 --- a/app/http/requests/container/id.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type ID struct { - ID string `form:"id" json:"id"` -} - -func (r *ID) Authorize(ctx http.Context) error { - return nil -} - -func (r *ID) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|string", - } -} - -func (r *ID) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/image_pull.go b/app/http/requests/container/image_pull.go deleted file mode 100644 index 7c9c2586..00000000 --- a/app/http/requests/container/image_pull.go +++ /dev/null @@ -1,42 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type ImagePull struct { - Name string `form:"name" json:"name"` - Auth bool `form:"auth" json:"auth"` - Username string `form:"username" json:"username"` - Password string `form:"password" json:"password"` -} - -func (r *ImagePull) Authorize(ctx http.Context) error { - return nil -} - -func (r *ImagePull) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - "auth": "bool", - "username": "string", - "password": "string", - } -} - -func (r *ImagePull) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ImagePull) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ImagePull) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ImagePull) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/network_connect_disconnect.go b/app/http/requests/container/network_connect_disconnect.go deleted file mode 100644 index b90cea4f..00000000 --- a/app/http/requests/container/network_connect_disconnect.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type NetworkConnectDisConnect struct { - Network string `form:"network" json:"network"` - Container string `form:"container" json:"container"` -} - -func (r *NetworkConnectDisConnect) Authorize(ctx http.Context) error { - return nil -} - -func (r *NetworkConnectDisConnect) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "network": "required|string", - "container": "required|string", - } -} - -func (r *NetworkConnectDisConnect) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkConnectDisConnect) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkConnectDisConnect) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkConnectDisConnect) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/network_create.go b/app/http/requests/container/network_create.go deleted file mode 100644 index 8f63fa9b..00000000 --- a/app/http/requests/container/network_create.go +++ /dev/null @@ -1,48 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -type NetworkCreate struct { - Name string `form:"name" json:"name"` - Driver string `form:"driver" json:"driver"` - Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"` - Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"` - Labels []types.KV `form:"labels" json:"labels"` - Options []types.KV `form:"options" json:"options"` -} - -func (r *NetworkCreate) Authorize(ctx http.Context) error { - return nil -} - -func (r *NetworkCreate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - "driver": "required|string|in:bridge,overlay,macvlan,ipvlan", - "ipv4": "required", - "ipv6": "required", - "labels": "slice", - "options": "slice", - } -} - -func (r *NetworkCreate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkCreate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkCreate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NetworkCreate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/container/volume_create.go b/app/http/requests/container/volume_create.go deleted file mode 100644 index 244c022a..00000000 --- a/app/http/requests/container/volume_create.go +++ /dev/null @@ -1,44 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" - - "github.com/TheTNB/panel/v2/pkg/types" -) - -type VolumeCreate struct { - Name string `form:"name" json:"name"` - Driver string `form:"driver" json:"driver"` - Labels []types.KV `form:"labels" json:"labels"` - Options []types.KV `form:"options" json:"options"` -} - -func (r *VolumeCreate) Authorize(ctx http.Context) error { - return nil -} - -func (r *VolumeCreate) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - "driver": "required|string|in:local", - "labels": "slice", - "options": "slice", - } -} - -func (r *VolumeCreate) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *VolumeCreate) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *VolumeCreate) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *VolumeCreate) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/archive.go b/app/http/requests/file/archive.go deleted file mode 100644 index c01a6ca0..00000000 --- a/app/http/requests/file/archive.go +++ /dev/null @@ -1,39 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Archive struct { - Paths []string `form:"paths" json:"paths"` - File string `form:"file" json:"file"` -} - -func (r *Archive) Authorize(ctx http.Context) error { - return nil -} - -func (r *Archive) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "paths": "array", - "paths.*": `regex:^/.*$|path_exists`, - "file": `regex:^/.*$|path_not_exists`, - } -} - -func (r *Archive) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Archive) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Archive) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Archive) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/copy.go b/app/http/requests/file/copy.go deleted file mode 100644 index 1678ba40..00000000 --- a/app/http/requests/file/copy.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Copy struct { - Source string `form:"source" json:"source"` - Target string `form:"target" json:"target"` -} - -func (r *Copy) Authorize(ctx http.Context) error { - return nil -} - -func (r *Copy) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "source": `regex:^/.*$|path_exists`, - "target": `regex:^/.*$`, - } -} - -func (r *Copy) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Copy) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Copy) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Copy) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/exist.go b/app/http/requests/file/exist.go deleted file mode 100644 index 79c7ac64..00000000 --- a/app/http/requests/file/exist.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Exist struct { - Path string `form:"path" json:"path"` -} - -func (r *Exist) Authorize(ctx http.Context) error { - return nil -} - -func (r *Exist) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_exists`, - } -} - -func (r *Exist) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Exist) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Exist) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Exist) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/move.go b/app/http/requests/file/move.go deleted file mode 100644 index 2b528f88..00000000 --- a/app/http/requests/file/move.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Move struct { - Source string `form:"source" json:"source"` - Target string `form:"target" json:"target"` -} - -func (r *Move) Authorize(ctx http.Context) error { - return nil -} - -func (r *Move) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "source": `regex:^/.*$|path_exists`, - "target": `regex:^/.*$`, - } -} - -func (r *Move) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Move) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Move) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Move) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/not_exist.go b/app/http/requests/file/not_exist.go deleted file mode 100644 index edc07c9c..00000000 --- a/app/http/requests/file/not_exist.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type NotExist struct { - Path string `form:"path" json:"path"` -} - -func (r *NotExist) Authorize(ctx http.Context) error { - return nil -} - -func (r *NotExist) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_not_exists`, - } -} - -func (r *NotExist) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NotExist) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NotExist) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *NotExist) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/permission.go b/app/http/requests/file/permission.go deleted file mode 100644 index c3a2319b..00000000 --- a/app/http/requests/file/permission.go +++ /dev/null @@ -1,42 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Permission struct { - Path string `form:"path" json:"path"` - Mode string `form:"mode" json:"mode"` - Owner string `form:"owner" json:"owner"` - Group string `form:"group" json:"group"` -} - -func (r *Permission) Authorize(ctx http.Context) error { - return nil -} - -func (r *Permission) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_exists`, - "mode": "regex:^0[0-7]{3}$", - "owner": "regex:^[a-zA-Z0-9_-]+$", - "group": "regex:^[a-zA-Z0-9_-]+$", - } -} - -func (r *Permission) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Permission) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Permission) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Permission) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/save.go b/app/http/requests/file/save.go deleted file mode 100644 index 16be8f16..00000000 --- a/app/http/requests/file/save.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Save struct { - Path string `form:"path" json:"path"` - Content string `form:"content" json:"content"` -} - -func (r *Save) Authorize(ctx http.Context) error { - return nil -} - -func (r *Save) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_exists`, - "content": "required|string", - } -} - -func (r *Save) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Save) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Save) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Save) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/search.go b/app/http/requests/file/search.go deleted file mode 100644 index ac5bee53..00000000 --- a/app/http/requests/file/search.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Search struct { - Path string `form:"path" json:"path"` - KeyWord string `form:"keyword" json:"keyword"` -} - -func (r *Search) Authorize(ctx http.Context) error { - return nil -} - -func (r *Search) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$|path_exists`, - "keyword": "required|string", - } -} - -func (r *Search) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Search) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Search) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Search) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/un_archive.go b/app/http/requests/file/un_archive.go deleted file mode 100644 index 80a11d27..00000000 --- a/app/http/requests/file/un_archive.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UnArchive struct { - File string `form:"file" json:"file"` - Path string `form:"path" json:"path"` -} - -func (r *UnArchive) Authorize(ctx http.Context) error { - return nil -} - -func (r *UnArchive) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "file": `regex:^/.*$|path_exists`, - "path": `regex:^/.*$`, - } -} - -func (r *UnArchive) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UnArchive) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UnArchive) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UnArchive) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/file/upload.go b/app/http/requests/file/upload.go deleted file mode 100644 index 3b0860c7..00000000 --- a/app/http/requests/file/upload.go +++ /dev/null @@ -1,40 +0,0 @@ -package requests - -import ( - "mime/multipart" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Upload struct { - Path string `form:"path" json:"path"` - File *multipart.FileHeader `form:"file" json:"file"` -} - -func (r *Upload) Authorize(ctx http.Context) error { - return nil -} - -func (r *Upload) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "path": `regex:^/.*$`, - "file": "required", - } -} - -func (r *Upload) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Upload) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Upload) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Upload) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/frp/service.go b/app/http/requests/plugins/frp/service.go deleted file mode 100644 index 2f47777d..00000000 --- a/app/http/requests/plugins/frp/service.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Service struct { - Service string `form:"service" json:"service"` -} - -func (r *Service) Authorize(ctx http.Context) error { - return nil -} - -func (r *Service) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "service": "required|string|in:frps,frpc", - } -} - -func (r *Service) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Service) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Service) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Service) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/frp/update_config.go b/app/http/requests/plugins/frp/update_config.go deleted file mode 100644 index 03bc614c..00000000 --- a/app/http/requests/plugins/frp/update_config.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateConfig struct { - Service string `form:"service" json:"service"` - Config string `form:"config" json:"config"` -} - -func (r *UpdateConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "service": "required|string|in:frps,frpc", - "config": "required|string", - } -} - -func (r *UpdateConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/gitea/update_config.go b/app/http/requests/plugins/gitea/update_config.go deleted file mode 100644 index f54e5737..00000000 --- a/app/http/requests/plugins/gitea/update_config.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateConfig struct { - Config string `form:"config" json:"config"` -} - -func (r *UpdateConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "config": "required|string", - } -} - -func (r *UpdateConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/podman/update_registry_config.go b/app/http/requests/plugins/podman/update_registry_config.go deleted file mode 100644 index 56e947ef..00000000 --- a/app/http/requests/plugins/podman/update_registry_config.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateRegistryConfig struct { - Config string `form:"config" json:"config"` -} - -func (r *UpdateRegistryConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateRegistryConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "config": "required|string", - } -} - -func (r *UpdateRegistryConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateRegistryConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateRegistryConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateRegistryConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/podman/update_storage_config.go b/app/http/requests/plugins/podman/update_storage_config.go deleted file mode 100644 index 646a6f8d..00000000 --- a/app/http/requests/plugins/podman/update_storage_config.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateStorageConfig struct { - Config string `form:"config" json:"config"` -} - -func (r *UpdateStorageConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateStorageConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "config": "required|string", - } -} - -func (r *UpdateStorageConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateStorageConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateStorageConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateStorageConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/rsync/create.go b/app/http/requests/plugins/rsync/create.go deleted file mode 100644 index c4be10f1..00000000 --- a/app/http/requests/plugins/rsync/create.go +++ /dev/null @@ -1,46 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Create struct { - Name string `form:"name" json:"name"` - Path string `form:"path" json:"path"` - Comment string `form:"comment" json:"comment"` - AuthUser string `form:"auth_user" json:"auth_user"` - Secret string `form:"secret" json:"secret"` - HostsAllow string `form:"hosts_allow" json:"hosts_allow"` -} - -func (r *Create) Authorize(ctx http.Context) error { - return nil -} - -func (r *Create) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|regex:^[a-zA-Z0-9-_]+$", - "path": `regex:^/.*$`, - "comment": "string", - "auth_user": "required|regex:^[a-zA-Z0-9-_]+$", - "secret": "required|min_len:8", - "hosts_allow": "string", - } -} - -func (r *Create) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Create) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Create) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Create) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/rsync/update.go b/app/http/requests/plugins/rsync/update.go deleted file mode 100644 index ba00a271..00000000 --- a/app/http/requests/plugins/rsync/update.go +++ /dev/null @@ -1,46 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Update struct { - Name string `form:"name" json:"name"` - Path string `form:"path" json:"path"` - Comment string `form:"comment" json:"comment"` - AuthUser string `form:"auth_user" json:"auth_user"` - Secret string `form:"secret" json:"secret"` - HostsAllow string `form:"hosts_allow" json:"hosts_allow"` -} - -func (r *Update) Authorize(ctx http.Context) error { - return nil -} - -func (r *Update) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|regex:^[a-zA-Z0-9-_]+$", - "path": `regex:^/.*$`, - "comment": "string", - "auth_user": "required|regex:^[a-zA-Z0-9-_]+$", - "secret": "required|min_len:8", - "hosts_allow": "string", - } -} - -func (r *Update) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Update) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Update) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Update) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/plugins/rsync/update_config.go b/app/http/requests/plugins/rsync/update_config.go deleted file mode 100644 index f54e5737..00000000 --- a/app/http/requests/plugins/rsync/update_config.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type UpdateConfig struct { - Config string `form:"config" json:"config"` -} - -func (r *UpdateConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *UpdateConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "config": "required|string", - } -} - -func (r *UpdateConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *UpdateConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/setting/https.go b/app/http/requests/setting/https.go deleted file mode 100644 index 4fe64dc6..00000000 --- a/app/http/requests/setting/https.go +++ /dev/null @@ -1,40 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Https struct { - Https bool `form:"https" json:"https"` - Cert string `form:"cert" json:"cert"` - Key string `form:"key" json:"key"` -} - -func (r *Https) Authorize(ctx http.Context) error { - return nil -} - -func (r *Https) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "https": "bool", - "cert": "string", - "key": "string", - } -} - -func (r *Https) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Https) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Https) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Https) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/setting/update.go b/app/http/requests/setting/update.go deleted file mode 100644 index 240ee997..00000000 --- a/app/http/requests/setting/update.go +++ /dev/null @@ -1,57 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Update struct { - Name string `form:"name" json:"name"` - Language string `form:"language" json:"language"` - Port uint `form:"port" json:"port"` - BackupPath string `form:"backup_path" json:"backup_path"` - WebsitePath string `form:"website_path" json:"website_path"` - Entrance string `form:"entrance" json:"entrance"` - UserName string `form:"username" json:"username"` - Email string `form:"email" json:"email"` - Password string `form:"password" json:"password"` -} - -func (r *Update) Authorize(ctx http.Context) error { - return nil -} - -func (r *Update) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string:2,20", - "language": "required|in:zh_CN,en", - "port": "required|int:1000,65535", - "backup_path": "required|string:2,255", - "website_path": "required|string:2,255", - "entrance": `required|regex:^/(\w+)?$|not_in:/api`, - "username": "required|string:2,20", - "email": "required|email", - "password": "string:8,255", - } -} - -func (r *Update) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "port": "uint", - } -} - -func (r *Update) Messages(ctx http.Context) map[string]string { - return map[string]string{ - "port.int": "port 值必须是一个整数且在 1000 - 65535 之间", - "password.string": "password 必须是一个字符串且长度在 8 - 255 之间", - } -} - -func (r *Update) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Update) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/user/login.go b/app/http/requests/user/login.go deleted file mode 100644 index 5b27edf9..00000000 --- a/app/http/requests/user/login.go +++ /dev/null @@ -1,42 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Login struct { - Username string `json:"username" form:"username"` - Password string `json:"password" form:"password"` -} - -func (r *Login) Authorize(ctx http.Context) error { - return nil -} - -func (r *Login) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "username": "required", - "password": "required|min_len:8", - } -} - -func (r *Login) Messages(ctx http.Context) map[string]string { - return map[string]string{ - "username.required": "用户名不能为空", - "password.required": "密码不能为空", - "password.min_len": "密码长度不能小于 8 位", - } -} - -func (r *Login) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Login) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Login) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/add.go b/app/http/requests/website/add.go deleted file mode 100644 index 03b23299..00000000 --- a/app/http/requests/website/add.go +++ /dev/null @@ -1,54 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Add struct { - Name string `form:"name" json:"name"` - Domains []string `form:"domains" json:"domains"` - Ports []uint `form:"ports" json:"ports"` - Path string `form:"path" json:"path"` - PHP string `form:"php" json:"php"` - DB bool `form:"db" json:"db"` - DBType string `form:"db_type" json:"db_type"` - DBName string `form:"db_name" json:"db_name"` - DBUser string `form:"db_user" json:"db_user"` - DBPassword string `form:"db_password" json:"db_password"` -} - -func (r *Add) Authorize(ctx http.Context) error { - return nil -} - -func (r *Add) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$|not_exists:websites,name|not_in:phpmyadmin,mysql,panel,ssh", - "domains": "required|slice", - "ports": "required|slice", - "path": `regex:^/.*$`, - "php": "required", - "db": "bool", - "db_type": "required_if:db,true|in:0,mysql,postgresql", - "db_name": "required_if:db,true|regex:^[a-zA-Z0-9_-]+$", - "db_user": "required_if:db,true|regex:^[a-zA-Z0-9_-]+$", - "db_password": "required_if:db,true|min_len:8", - } -} - -func (r *Add) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Add) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Add) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Add) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/delete.go b/app/http/requests/website/delete.go deleted file mode 100644 index dece27f5..00000000 --- a/app/http/requests/website/delete.go +++ /dev/null @@ -1,42 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type Delete struct { - ID uint `form:"id" json:"id"` - Path bool `form:"path" json:"path"` - DB bool `form:"db" json:"db"` -} - -func (r *Delete) Authorize(ctx http.Context) error { - return nil -} - -func (r *Delete) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:websites,id", - "path": "bool", - "db": "bool", - } -} - -func (r *Delete) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - } -} - -func (r *Delete) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Delete) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *Delete) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/delete_backup.go b/app/http/requests/website/delete_backup.go deleted file mode 100644 index 62c0ab13..00000000 --- a/app/http/requests/website/delete_backup.go +++ /dev/null @@ -1,36 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type DeleteBackup struct { - Name string `form:"name" json:"name"` -} - -func (r *DeleteBackup) Authorize(ctx http.Context) error { - return nil -} - -func (r *DeleteBackup) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "name": "required|string", - } -} - -func (r *DeleteBackup) Filters(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DeleteBackup) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DeleteBackup) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *DeleteBackup) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/id.go b/app/http/requests/website/id.go deleted file mode 100644 index 65fdc9fa..00000000 --- a/app/http/requests/website/id.go +++ /dev/null @@ -1,38 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type ID struct { - ID uint `form:"id" json:"id"` -} - -func (r *ID) Authorize(ctx http.Context) error { - return nil -} - -func (r *ID) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:websites,id", - } -} - -func (r *ID) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - } -} - -func (r *ID) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *ID) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/restore_backup.go b/app/http/requests/website/restore_backup.go deleted file mode 100644 index 5c5ad2c8..00000000 --- a/app/http/requests/website/restore_backup.go +++ /dev/null @@ -1,40 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type RestoreBackup struct { - ID uint `form:"id" json:"id"` - Name string `form:"name" json:"name"` -} - -func (r *RestoreBackup) Authorize(ctx http.Context) error { - return nil -} - -func (r *RestoreBackup) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:websites,id", - "name": "required|string", - } -} - -func (r *RestoreBackup) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - } -} - -func (r *RestoreBackup) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *RestoreBackup) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *RestoreBackup) PrepareForValidation(ctx http.Context, data validation.Data) error { - return nil -} diff --git a/app/http/requests/website/save_config.go b/app/http/requests/website/save_config.go deleted file mode 100644 index ac794cf0..00000000 --- a/app/http/requests/website/save_config.go +++ /dev/null @@ -1,100 +0,0 @@ -package requests - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/validation" -) - -type SaveConfig struct { - ID uint `form:"id" json:"id"` - Domains []string `form:"domains" json:"domains"` - Ports []uint `form:"ports" json:"ports"` - SSLPorts []uint `form:"ssl_ports" json:"ssl_ports"` - QUICPorts []uint `form:"quic_ports" json:"quic_ports"` - OCSP bool `form:"ocsp" json:"ocsp"` - HSTS bool `form:"hsts" json:"hsts"` - SSL bool `form:"ssl" json:"ssl"` - HTTPRedirect bool `form:"http_redirect" json:"http_redirect"` - OpenBasedir bool `form:"open_basedir" json:"open_basedir"` - Waf bool `form:"waf" json:"waf"` - WafCache string `form:"waf_cache" json:"waf_cache"` - WafMode string `form:"waf_mode" json:"waf_mode"` - WafCcDeny string `form:"waf_cc_deny" json:"waf_cc_deny"` - Index string `form:"index" json:"index"` - Path string `form:"path" json:"path"` - Root string `form:"root" json:"root"` - Raw string `form:"raw" json:"raw"` - Rewrite string `form:"rewrite" json:"rewrite"` - PHP int `form:"php" json:"php"` - SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"` - SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"` -} - -func (r *SaveConfig) Authorize(ctx http.Context) error { - return nil -} - -func (r *SaveConfig) Rules(ctx http.Context) map[string]string { - return map[string]string{ - "id": "required|exists:websites,id", - "domains": "required|slice", - "ports": "required|slice", - "ssl_ports": "slice|not_in:80", - "quic_ports": "slice|not_in:80", - "ocsp": "bool", - "hsts": "bool", - "ssl": "bool", - "http_redirect": "bool", - "open_basedir": "bool", - "waf": "bool", - "waf_cache": "required|string", - "waf_mode": "required|string", - "waf_cc_deny": "required|string", - "index": "required|string", - "path": "required|string", - "root": "required|string", - "raw": "required|string", - "rewrite": "string", - "php": "int", - "ssl_certificate": "required_if:ssl,true", - "ssl_certificate_key": "required_if:ssl,true", - } -} - -func (r *SaveConfig) Filters(ctx http.Context) map[string]string { - return map[string]string{ - "id": "uint", - "php": "int", - } -} - -func (r *SaveConfig) Messages(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *SaveConfig) Attributes(ctx http.Context) map[string]string { - return map[string]string{} -} - -func (r *SaveConfig) PrepareForValidation(ctx http.Context, data validation.Data) error { - _, exist := data.Get("waf_mode") - if !exist { - if err := data.Set("waf_mode", "DYNAMIC"); err != nil { - return err - } - } - _, exist = data.Get("waf_cc_deny") - if !exist { - if err := data.Set("waf_cc_deny", "rate=1000r/m duration=60m"); err != nil { - return err - } - } - _, exist = data.Get("waf_cache") - if !exist { - if err := data.Set("waf_cache", "capacity=50"); err != nil { - return err - } - } - - return nil -} diff --git a/app/jobs/process_task.go b/app/jobs/process_task.go deleted file mode 100644 index 41d273cc..00000000 --- a/app/jobs/process_task.go +++ /dev/null @@ -1,74 +0,0 @@ -package jobs - -import ( - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/shell" -) - -// ProcessTask 处理面板任务 -type ProcessTask struct { -} - -// Signature The name and signature of the job. -func (receiver *ProcessTask) Signature() string { - return "process_task" -} - -// Handle Execute the job. -func (receiver *ProcessTask) Handle(args ...any) error { - taskID, ok := args[0].(uint) - if !ok { - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "args": args, - }).Infof("参数错误") - return nil - } - - var task models.Task - _ = facades.Orm().Query().Where("id = ?", taskID).Get(&task) - if task.ID == 0 { - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - }).Infof("任务不存在") - return nil - } - - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - }).Infof("开始执行任务") - - task.Status = models.TaskStatusRunning - if err := facades.Orm().Query().Save(&task); err != nil { - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - "error": err.Error(), - }).Infof("更新任务状态失败") - return nil - } - - if _, err := shell.Execf(task.Shell); err != nil { - task.Status = models.TaskStatusFailed - _ = facades.Orm().Query().Save(&task) - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - "error": err.Error(), - }).Infof("执行任务失败") - return nil - } - - task.Status = models.TaskStatusSuccess - if err := facades.Orm().Query().Save(&task); err != nil { - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - "error": err.Error(), - }).Infof("更新任务状态失败") - return nil - } - - facades.Log().Tags("面板", "异步任务").With(map[string]any{ - "task_id": taskID, - }).Infof("执行任务成功") - return nil -} diff --git a/app/models/cert.go b/app/models/cert.go deleted file mode 100644 index bdcaab65..00000000 --- a/app/models/cert.go +++ /dev/null @@ -1,22 +0,0 @@ -package models - -import ( - "github.com/goravel/framework/database/orm" -) - -type Cert struct { - orm.Model - UserID uint `gorm:"not null" json:"user_id"` // 关联的 ACME 用户 ID - WebsiteID uint `gorm:"not null" json:"website_id"` // 关联的网站 ID - DNSID uint `gorm:"not null" json:"dns_id"` // 关联的 DNS ID - Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 4096) - Domains []string `gorm:"not null;serializer:json" json:"domains"` - AutoRenew bool `gorm:"not null" json:"auto_renew"` // 自动续签 - CertURL string `gorm:"not null" json:"cert_url"` // 证书 URL (续签时使用) - Cert string `gorm:"not null" json:"cert"` // 证书内容 - Key string `gorm:"not null" json:"key"` // 私钥内容 - - Website *Website `gorm:"foreignKey:WebsiteID" json:"website"` - User *CertUser `gorm:"foreignKey:UserID" json:"user"` - DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"` -} diff --git a/app/models/cert_dns.go b/app/models/cert_dns.go deleted file mode 100644 index 014d33f2..00000000 --- a/app/models/cert_dns.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -import ( - "github.com/goravel/framework/database/orm" - - "github.com/TheTNB/panel/v2/pkg/acme" -) - -type CertDNS struct { - orm.Model - Name string `gorm:"not null" json:"name"` // 备注名称 - Type string `gorm:"not null" json:"type"` // DNS 提供商 (dnspod, tencent, aliyun, cloudflare) - Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"` - - Certs []*Cert `gorm:"foreignKey:DNSID" json:"-"` -} diff --git a/app/models/cert_user.go b/app/models/cert_user.go deleted file mode 100644 index 9a9c881c..00000000 --- a/app/models/cert_user.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type CertUser struct { - orm.Model - Email string `gorm:"not null" json:"email"` - CA string `gorm:"not null" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass) - Kid string `gorm:"not null" json:"kid"` - HmacEncoded string `gorm:"not null" json:"hmac_encoded"` - PrivateKey string `gorm:"not null" json:"private_key"` - KeyType string `gorm:"not null" json:"key_type"` - - Certs []*Cert `gorm:"foreignKey:UserID" json:"-"` -} diff --git a/app/models/cron.go b/app/models/cron.go deleted file mode 100644 index 5d22f5eb..00000000 --- a/app/models/cron.go +++ /dev/null @@ -1,13 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type Cron struct { - orm.Model - Name string `gorm:"not null;unique" json:"name"` - Status bool `gorm:"not null" json:"status"` - Type string `gorm:"not null" json:"type"` - Time string `gorm:"not null" json:"time"` - Shell string `gorm:"not null" json:"shell"` - Log string `gorm:"not null" json:"log"` -} diff --git a/app/models/database.go b/app/models/database.go deleted file mode 100644 index 57c3e85e..00000000 --- a/app/models/database.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type Database struct { - orm.Model - Name string `gorm:"not null;unique" json:"name"` - Type string `gorm:"not null" json:"type"` - Host string `gorm:"not null" json:"host"` - Port int `gorm:"not null" json:"port"` - Username string `gorm:"not null" json:"username"` - Password string `gorm:"not null" json:"password"` - Remark string `gorm:"not null" json:"remark"` -} diff --git a/app/models/monitor.go b/app/models/monitor.go deleted file mode 100644 index e8566083..00000000 --- a/app/models/monitor.go +++ /dev/null @@ -1,12 +0,0 @@ -package models - -import ( - "github.com/goravel/framework/database/orm" - - "github.com/TheTNB/panel/v2/pkg/tools" -) - -type Monitor struct { - orm.Model - Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"` -} diff --git a/app/models/plugin.go b/app/models/plugin.go deleted file mode 100644 index 87ac7370..00000000 --- a/app/models/plugin.go +++ /dev/null @@ -1,11 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type Plugin struct { - orm.Model - Slug string `gorm:"not null;unique" json:"slug"` - Version string `gorm:"not null" json:"version"` - Show bool `gorm:"not null" json:"show"` - ShowOrder int `gorm:"not null" json:"show_order"` -} diff --git a/app/models/setting.go b/app/models/setting.go deleted file mode 100644 index 04081bd2..00000000 --- a/app/models/setting.go +++ /dev/null @@ -1,23 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -const ( - SettingKeyName = "name" - SettingKeyVersion = "version" - SettingKeyMonitor = "monitor" - SettingKeyMonitorDays = "monitor_days" - SettingKeyBackupPath = "backup_path" - SettingKeyWebsitePath = "website_path" - SettingKeyMysqlRootPassword = "mysql_root_password" - SettingKeySshHost = "ssh_host" - SettingKeySshPort = "ssh_port" - SettingKeySshUser = "ssh_user" - SettingKeySshPassword = "ssh_password" -) - -type Setting struct { - orm.Model - Key string `gorm:"not null;unique" json:"key"` - Value string `gorm:"not null" json:"value"` -} diff --git a/app/models/task.go b/app/models/task.go deleted file mode 100644 index 87896048..00000000 --- a/app/models/task.go +++ /dev/null @@ -1,18 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -const ( - TaskStatusWaiting = "waiting" - TaskStatusRunning = "running" - TaskStatusSuccess = "finished" - TaskStatusFailed = "failed" -) - -type Task struct { - orm.Model - Name string `gorm:"not null;index" json:"name"` - Status string `gorm:"not null;default:'waiting'" json:"status"` - Shell string `gorm:"not null" json:"shell"` - Log string `gorm:"not null" json:"log"` -} diff --git a/app/models/user.go b/app/models/user.go deleted file mode 100644 index 55095690..00000000 --- a/app/models/user.go +++ /dev/null @@ -1,10 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type User struct { - orm.Model - Username string `gorm:"not null;unique" json:"username"` - Password string `gorm:"not null" json:"password"` - Email string `gorm:"not null" json:"email"` -} diff --git a/app/models/website.go b/app/models/website.go deleted file mode 100644 index eface35c..00000000 --- a/app/models/website.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -import "github.com/goravel/framework/database/orm" - -type Website struct { - orm.Model - Name string `gorm:"not null;unique" json:"name"` - Status bool `gorm:"not null;default:true" json:"status"` - Path string `gorm:"not null" json:"path"` - PHP int `gorm:"not null" json:"php"` - SSL bool `gorm:"not null" json:"ssl"` - Remark string `gorm:"not null" json:"remark"` - - Cert *Cert `gorm:"foreignKey:WebsiteID" json:"cert"` -} diff --git a/app/plugins/README.md b/app/plugins/README.md deleted file mode 100644 index 0e6118a9..00000000 --- a/app/plugins/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# 面板插件目录 - -文档待定 diff --git a/app/plugins/fail2ban/controller.go b/app/plugins/fail2ban/controller.go deleted file mode 100644 index 9df9ac43..00000000 --- a/app/plugins/fail2ban/controller.go +++ /dev/null @@ -1,342 +0,0 @@ -package openresty - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Controller struct { - website internal.Website -} - -func NewController() *Controller { - return &Controller{ - website: services.NewWebsiteImpl(), - } -} - -// List 所有 Fail2ban 规则 -func (r *Controller) List(ctx http.Context) http.Response { - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - - jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1) - if len(jailList) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "Fail2ban 规则为空") - } - - var jails []types.Fail2banJail - for i, jail := range jailList { - if i == 0 { - continue - } - - jailName := jail[1] - jailRaw := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END") - if len(jailRaw) == 0 { - continue - } - jailEnabled := strings.Contains(jailRaw, "enabled = true") - jailLogPath := regexp.MustCompile(`logpath = (.*)`).FindStringSubmatch(jailRaw) - jailMaxRetry := regexp.MustCompile(`maxretry = (.*)`).FindStringSubmatch(jailRaw) - jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw) - jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw) - - jails = append(jails, types.Fail2banJail{ - Name: jailName, - Enabled: jailEnabled, - LogPath: jailLogPath[1], - MaxRetry: cast.ToInt(jailMaxRetry[1]), - FindTime: cast.ToInt(jailFindTime[1]), - BanTime: cast.ToInt(jailBanTime[1]), - }) - } - - paged, total := h.Paginate(ctx, jails) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Add 添加 Fail2ban 规则 -func (r *Controller) Add(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required", - "type": "required|in:website,service", - "maxretry": "required", - "findtime": "required", - "bantime": "required", - "website_name": "required_if:type,website", - "website_mode": "required_if:type,website", - "website_path": "required_if:website_mode,path", - }); sanitize != nil { - return sanitize - } - - jailName := ctx.Request().Input("name") - jailType := ctx.Request().Input("type") - jailMaxRetry := ctx.Request().Input("maxretry") - jailFindTime := ctx.Request().Input("findtime") - jailBanTime := ctx.Request().Input("bantime") - jailWebsiteName := ctx.Request().Input("website_name") - jailWebsiteMode := ctx.Request().Input("website_mode") - jailWebsitePath := ctx.Request().Input("website_path") - - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - if (strings.Contains(raw, "["+jailName+"]") && jailType == "service") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-cc") && jailType == "website" && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-path") && jailType == "website" && jailWebsiteMode == "path") { - return h.Error(ctx, http.StatusUnprocessableEntity, "规则已存在") - } - - switch jailType { - case "website": - var website models.Website - err := facades.Orm().Query().Where("name", jailWebsiteName).FirstOrFail(&website) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "网站不存在") - } - config, err := r.website.GetConfig(website.ID) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取网站配置失败") - } - var ports string - for _, port := range config.Ports { - fields := strings.Fields(cast.ToString(port)) - ports += fields[0] + "," - } - - rule := ` -# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START -[` + jailWebsiteName + `-` + jailWebsiteMode + `] -enabled = true -filter = haozi-` + jailWebsiteName + `-` + jailWebsiteMode + ` -port = ` + ports + ` -maxretry = ` + jailMaxRetry + ` -findtime = ` + jailFindTime + ` -bantime = ` + jailBanTime + ` -action = %(action_mwl)s -logpath = /www/wwwlogs/` + website.Name + `.log -# ` + jailWebsiteName + `-` + jailWebsiteMode + `-END -` - raw += rule - if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - - var filter string - if jailWebsiteMode == "cc" { - filter = ` -[Definition] -failregex = ^\s-.*HTTP/.*$ -ignoreregex = -` - } else { - filter = ` -[Definition] -failregex = ^\s-.*\s` + jailWebsitePath + `.*HTTP/.*$ -ignoreregex = -` - } - if err = io.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - - case "service": - var logPath string - var filter string - var port string - var err error - switch jailName { - case "ssh": - if os.IsDebian() || os.IsUbuntu() { - logPath = "/var/log/auth.log" - } else { - logPath = "/var/log/secure" - } - filter = "sshd" - port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") - case "mysql": - logPath = "/www/server/mysql/mysql-error.log" - filter = "mysqld-auth" - port, err = shell.Execf("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'") - case "pure-ftpd": - logPath = "/var/log/messages" - filter = "pure-ftpd" - port, err = shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`) - default: - return h.Error(ctx, http.StatusUnprocessableEntity, "未知服务") - } - if len(port) == 0 || err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装") - } - - rule := ` -# ` + jailName + `-START -[` + jailName + `] -enabled = true -filter = ` + filter + ` -port = ` + port + ` -maxretry = ` + jailMaxRetry + ` -findtime = ` + jailFindTime + ` -bantime = ` + jailBanTime + ` -action = %(action_mwl)s -logpath = ` + logPath + ` -# ` + jailName + `-END -` - raw += rule - if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - } - - if _, err := shell.Execf("fail2ban-client reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载配置失败") - } - - return h.Success(ctx, nil) -} - -// Delete 删除规则 -func (r *Controller) Delete(ctx http.Context) http.Response { - jailName := ctx.Request().Input("name") - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - if !strings.Contains(raw, "["+jailName+"]") { - return h.Error(ctx, http.StatusUnprocessableEntity, "规则不存在") - } - - rule := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END") - raw = strings.Replace(raw, "\n# "+jailName+"-START"+rule+"# "+jailName+"-END", "", -1) - raw = strings.TrimSpace(raw) - if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - - if _, err := shell.Execf("fail2ban-client reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载配置失败") - } - - return h.Success(ctx, nil) -} - -// BanList 获取封禁列表 -func (r *Controller) BanList(ctx http.Context) http.Response { - name := ctx.Request().Input("name") - if len(name) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "缺少参数") - } - - currentlyBan, err := shell.Execf(`fail2ban-client status %s | grep "Currently banned" | awk '{print $4}'`, name) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败") - } - totalBan, err := shell.Execf(`fail2ban-client status %s | grep "Total banned" | awk '{print $4}'`, name) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败") - } - bannedIp, err := shell.Execf(`fail2ban-client status %s | grep "Banned IP list" | awk -F ":" '{print $2}'`, name) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败") - } - bannedIpList := strings.Split(bannedIp, " ") - - var list []map[string]string - for _, ip := range bannedIpList { - if len(ip) > 0 { - list = append(list, map[string]string{ - "name": name, - "ip": ip, - }) - } - } - if list == nil { - list = []map[string]string{} - } - - return h.Success(ctx, http.Json{ - "currently_ban": currentlyBan, - "total_ban": totalBan, - "baned_list": list, - }) -} - -// Unban 解封 -func (r *Controller) Unban(ctx http.Context) http.Response { - name := ctx.Request().Input("name") - ip := ctx.Request().Input("ip") - if len(name) == 0 || len(ip) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "缺少参数") - } - - if _, err := shell.Execf("fail2ban-client set %s unbanip %s", name, ip); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "解封失败") - } - - return h.Success(ctx, nil) -} - -// SetWhiteList 设置白名单 -func (r *Controller) SetWhiteList(ctx http.Context) http.Response { - ip := ctx.Request().Input("ip") - if len(ip) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "缺少参数") - } - - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - // 正则替换 - reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`) - if reg.MatchString(raw) { - raw = reg.ReplaceAllString(raw, "ignoreip = "+ip+"\n") - } else { - return h.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏") - } - - if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败") - } - - if _, err := shell.Execf("fail2ban-client reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载配置失败") - } - return h.Success(ctx, nil) -} - -// GetWhiteList 获取白名单 -func (r *Controller) GetWhiteList(ctx http.Context) http.Response { - raw, err := io.Read("/etc/fail2ban/jail.local") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`) - if reg.MatchString(raw) { - ignoreIp := reg.FindStringSubmatch(raw)[1] - return h.Success(ctx, ignoreIp) - } else { - return h.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏") - } -} diff --git a/app/plugins/fail2ban/main.go b/app/plugins/fail2ban/main.go deleted file mode 100644 index ab3621c5..00000000 --- a/app/plugins/fail2ban/main.go +++ /dev/null @@ -1,39 +0,0 @@ -package openresty - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/route" - - "github.com/TheTNB/panel/v2/app/http/middleware" - "github.com/TheTNB/panel/v2/app/plugins/loader" - "github.com/TheTNB/panel/v2/pkg/types" -) - -func init() { - loader.Register(&types.Plugin{ - Name: "Fail2ban", - Description: "Fail2ban 扫描系统日志文件并从中找出多次尝试失败的IP地址,将该IP地址加入防火墙的拒绝访问列表中", - Slug: "fail2ban", - Version: "1.0.2", - Requires: []string{}, - Excludes: []string{}, - Install: `bash /www/panel/scripts/fail2ban/install.sh`, - Uninstall: `bash /www/panel/scripts/fail2ban/uninstall.sh`, - Update: `bash /www/panel/scripts/fail2ban/update.sh`, - Boot: func(app foundation.Application) { - RouteFacade := app.MakeRoute() - RouteFacade.Prefix("api/plugins/fail2ban").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - r.Prefix("openresty").Group(func(route route.Router) { - controller := NewController() - route.Get("jails", controller.List) - route.Post("jails", controller.Add) - route.Delete("jails", controller.Delete) - route.Get("jails/{name}", controller.BanList) - route.Post("unban", controller.Unban) - route.Post("whiteList", controller.SetWhiteList) - route.Get("whiteList", controller.GetWhiteList) - }) - }) - }, - }) -} diff --git a/app/plugins/frp_controller.go b/app/plugins/frp_controller.go deleted file mode 100644 index e199d8a1..00000000 --- a/app/plugins/frp_controller.go +++ /dev/null @@ -1,72 +0,0 @@ -package plugins - -import ( - "fmt" - - "github.com/goravel/framework/contracts/http" - - requests "github.com/TheTNB/panel/v2/app/http/requests/plugins/frp" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type FrpController struct { -} - -func NewFrpController() *FrpController { - return &FrpController{} -} - -// GetConfig -// -// @Summary 获取配置 -// @Description 获取 Frp 配置 -// @Tags 插件-Frp -// @Produce json -// @Security BearerToken -// @Param service query string false "服务" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/frp/config [get] -func (r *FrpController) GetConfig(ctx http.Context) http.Response { - var serviceRequest requests.Service - sanitize := h.SanitizeRequest(ctx, &serviceRequest) - if sanitize != nil { - return sanitize - } - - config, err := io.Read(fmt.Sprintf("/www/server/frp/%s.toml", serviceRequest.Service)) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateConfig -// -// @Summary 更新配置 -// @Description 更新 Frp 配置 -// @Tags 插件-Frp -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/frp/config [post] -func (r *FrpController) UpdateConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write(fmt.Sprintf("/www/server/frp/%s.toml", updateRequest.Service), updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart(updateRequest.Service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/gitea_controller.go b/app/plugins/gitea_controller.go deleted file mode 100644 index ad361949..00000000 --- a/app/plugins/gitea_controller.go +++ /dev/null @@ -1,63 +0,0 @@ -package plugins - -import ( - "github.com/goravel/framework/contracts/http" - - requests "github.com/TheTNB/panel/v2/app/http/requests/plugins/gitea" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type GiteaController struct { -} - -func NewGiteaController() *GiteaController { - return &GiteaController{} -} - -// GetConfig -// -// @Summary 获取配置 -// @Description 获取 Gitea 配置 -// @Tags 插件-Gitea -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/gitea/config [get] -func (r *GiteaController) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/www/server/gitea/app.ini") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateConfig -// -// @Summary 更新配置 -// @Description 更新 Gitea 配置 -// @Tags 插件-Gitea -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/gitea/config [post] -func (r *GiteaController) UpdateConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write("/www/server/gitea/app.ini", updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart("gitea"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/loader/loader.go b/app/plugins/loader/loader.go deleted file mode 100644 index 21efd81d..00000000 --- a/app/plugins/loader/loader.go +++ /dev/null @@ -1,17 +0,0 @@ -package loader - -import ( - "github.com/TheTNB/panel/v2/pkg/types" -) - -var data []*types.Plugin - -// All 获取所有插件 -func All() []*types.Plugin { - return data -} - -// Register 注册插件 -func Register(plugin *types.Plugin) { - data = append(data, plugin) -} diff --git a/app/plugins/main.go b/app/plugins/main.go deleted file mode 100644 index 0595f1c3..00000000 --- a/app/plugins/main.go +++ /dev/null @@ -1,6 +0,0 @@ -package plugins - -import _ "github.com/TheTNB/panel/v2/app/plugins/openresty" - -// Boot 启动所有插件 -func Boot() {} diff --git a/app/plugins/mysql_controller.go b/app/plugins/mysql_controller.go deleted file mode 100644 index d2e60215..00000000 --- a/app/plugins/mysql_controller.go +++ /dev/null @@ -1,506 +0,0 @@ -package plugins - -import ( - "fmt" - "regexp" - - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/db" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type MySQLController struct { - setting internal.Setting - backup internal.Backup -} - -func NewMySQLController() *MySQLController { - return &MySQLController{ - setting: services.NewSettingImpl(), - backup: services.NewBackupImpl(), - } -} - -// GetConfig 获取配置 -func (r *MySQLController) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/www/server/mysql/conf/my.cnf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败") - } - - return h.Success(ctx, config) -} - -// SaveConfig 保存配置 -func (r *MySQLController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空") - } - - if err := io.Write("/www/server/mysql/conf/my.cnf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败") - } - - if err := systemctl.Reload("mysqld"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载MySQL失败") - } - - return h.Success(ctx, nil) -} - -// Load 获取负载 -func (r *MySQLController) Load(ctx http.Context) http.Response { - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - if len(rootPassword) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空") - } - - status, _ := systemctl.Status("mysqld") - if !status { - return h.Success(ctx, []types.NV{}) - } - - raw, err := shell.Execf("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败") - } - - var data []map[string]string - expressions := []struct { - regex string - name string - }{ - {`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"}, - {`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"}, - {`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"}, - {`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"}, - {`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"}, - {`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"}, - {`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"}, - {`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"}, - {`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"}, - {`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"}, - {`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"}, - {`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"}, - {`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"}, - {`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"}, - {`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"}, - {`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"}, - {`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"}, - {`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"}, - } - - for _, expression := range expressions { - re := regexp.MustCompile(expression.regex) - matches := re.FindStringSubmatch(raw) - if len(matches) > 1 { - d := map[string]string{"name": expression.name, "value": matches[1]} - if expression.name == "发送" || expression.name == "接收" { - d["value"] = str.FormatBytes(cast.ToFloat64(matches[1])) - } - - data = append(data, d) - } - } - - // 索引命中率 - readRequests := cast.ToFloat64(data[9]["value"]) - reads := cast.ToFloat64(data[10]["value"]) - data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100) - // Innodb 索引命中率 - bufferPoolReads := cast.ToFloat64(data[11]["value"]) - bufferPoolReadRequests := cast.ToFloat64(data[12]["value"]) - data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100) - - return h.Success(ctx, data) -} - -// ErrorLog 获取错误日志 -func (r *MySQLController) ErrorLog(ctx http.Context) http.Response { - log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-error.log") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearErrorLog 清空错误日志 -func (r *MySQLController) ClearErrorLog(ctx http.Context) http.Response { - if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-error.log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// SlowLog 获取慢查询日志 -func (r *MySQLController) SlowLog(ctx http.Context) http.Response { - log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-slow.log") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearSlowLog 清空慢查询日志 -func (r *MySQLController) ClearSlowLog(ctx http.Context) http.Response { - if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-slow.log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - return h.Success(ctx, nil) -} - -// GetRootPassword 获取root密码 -func (r *MySQLController) GetRootPassword(ctx http.Context) http.Response { - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - if len(rootPassword) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空") - } - - return h.Success(ctx, rootPassword) -} - -// SetRootPassword 设置root密码 -func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response { - rootPassword := ctx.Request().Input("password") - if len(rootPassword) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空") - } - - oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - mysql, err := db.NewMySQL("root", oldRootPassword, r.getSock(), "unix") - if err != nil { - // 尝试安全模式直接改密 - if err = db.MySQLResetRootPassword(rootPassword); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } else { - if err = mysql.UserPassword("root", rootPassword); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err)) - } - - return h.Success(ctx, nil) -} - -// DatabaseList 获取数据库列表 -func (r *MySQLController) DatabaseList(ctx http.Context) http.Response { - password := r.setting.Get(models.SettingKeyMysqlRootPassword) - mysql, err := db.NewMySQL("root", password, r.getSock(), "unix") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []types.MySQLDatabase{}, - }) - } - - databases, err := mysql.Databases() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败") - } - paged, total := h.Paginate(ctx, databases) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddDatabase 添加数据库 -func (r *MySQLController) AddDatabase(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$", - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:32", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - database := ctx.Request().Input("database") - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.DatabaseCreate(database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.UserCreate(user, password); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.PrivilegesGrant(user, database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteDatabase 删除数据库 -func (r *MySQLController) DeleteDatabase(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - database := ctx.Request().Input("database") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.DatabaseDrop(database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// BackupList 获取备份列表 -func (r *MySQLController) BackupList(ctx http.Context) http.Response { - backups, err := r.backup.MysqlList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, backups) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// UploadBackup 上传备份 -func (r *MySQLController) UploadBackup(ctx http.Context) http.Response { - file, err := ctx.Request().File("file") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败") - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql" - if !io.Exists(backupPath) { - if err = io.Mkdir(backupPath, 0644); err != nil { - return nil - } - } - - name := file.GetClientOriginalName() - _, err = file.StoreAs(backupPath, name) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败") - } - - return h.Success(ctx, nil) -} - -// CreateBackup 创建备份 -func (r *MySQLController) CreateBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys", - }); sanitize != nil { - return sanitize - } - - database := ctx.Request().Input("database") - if err := r.backup.MysqlBackup(database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteBackup 删除备份 -func (r *MySQLController) DeleteBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|min_len:1|max_len:255", - }); sanitize != nil { - return sanitize - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql" - fileName := ctx.Request().Input("name") - if err := io.Remove(backupPath + "/" + fileName); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// RestoreBackup 还原备份 -func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "backup": "required|min_len:1|max_len:255", - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys", - }); sanitize != nil { - return sanitize - } - - if err := r.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UserList 用户列表 -func (r *MySQLController) UserList(ctx http.Context) http.Response { - password := r.setting.Get(models.SettingKeyMysqlRootPassword) - mysql, err := db.NewMySQL("root", password, r.getSock(), "unix") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []types.MySQLUser{}, - }) - } - - users, err := mysql.Users() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取用户列表失败") - } - paged, total := h.Paginate(ctx, users) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddUser 添加用户 -func (r *MySQLController) AddUser(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$", - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:32", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - database := ctx.Request().Input("database") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.UserCreate(user, password); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.PrivilegesGrant(user, database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteUser 删除用户 -func (r *MySQLController) DeleteUser(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - user := ctx.Request().Input("user") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.UserDrop(user); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// SetUserPassword 设置用户密码 -func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:32", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.UserPassword(user, password); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// SetUserPrivileges 设置用户权限 -func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$", - "database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$", - }); sanitize != nil { - return sanitize - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - user := ctx.Request().Input("user") - database := ctx.Request().Input("database") - mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if err = mysql.PrivilegesGrant(user, database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// getSock 获取sock文件位置 -func (r *MySQLController) getSock() string { - if io.Exists("/tmp/mysql.sock") { - return "/tmp/mysql.sock" - } - if io.Exists("/www/server/mysql/config/my.cnf") { - config, _ := io.Read("/www/server/mysql/config/my.cnf") - re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`) - matches := re.FindStringSubmatch(config) - if len(matches) > 2 { - return matches[2] - } - } - if io.Exists("/etc/my.cnf") { - config, _ := io.Read("/etc/my.cnf") - re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`) - matches := re.FindStringSubmatch(config) - if len(matches) > 2 { - return matches[2] - } - } - - return "/tmp/mysql.sock" -} diff --git a/app/plugins/openresty/controller.go b/app/plugins/openresty/controller.go deleted file mode 100644 index f37414dd..00000000 --- a/app/plugins/openresty/controller.go +++ /dev/null @@ -1,187 +0,0 @@ -package openresty - -import ( - "fmt" - "regexp" - "time" - - "github.com/go-resty/resty/v2" - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Controller struct { - // Dependent services -} - -func NewController() *Controller { - return &Controller{} -} - -// GetConfig -// -// @Summary 获取配置 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/config [get] -func (r *Controller) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/www/server/openresty/conf/nginx.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取配置失败") - } - - return h.Success(ctx, config) -} - -// SaveConfig -// -// @Summary 保存配置 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Param config body string true "配置" -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/config [post] -func (r *Controller) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "配置不能为空") - } - - if err := io.Write("/www/server/openresty/conf/nginx.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "保存配置失败") - } - - if err := systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载服务失败: %v", err)) - } - - return h.Success(ctx, nil) -} - -// ErrorLog -// -// @Summary 获取错误日志 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/errorLog [get] -func (r *Controller) ErrorLog(ctx http.Context) http.Response { - if !io.Exists("/www/wwwlogs/nginx_error.log") { - return h.Success(ctx, "") - } - - out, err := shell.Execf("tail -n 100 /www/wwwlogs/openresty_error.log") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, out) -} - -// ClearErrorLog -// -// @Summary 清空错误日志 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/clearErrorLog [post] -func (r *Controller) ClearErrorLog(ctx http.Context) http.Response { - if out, err := shell.Execf("echo '' > /www/wwwlogs/openresty_error.log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// Load -// -// @Summary 获取负载状态 -// @Tags 插件-OpenResty -// @Produce json -// @Security BearerToken -// @Success 200 {object} h.SuccessResponse -// @Router /plugins/openresty/load [get] -func (r *Controller) Load(ctx http.Context) http.Response { - client := resty.New().SetTimeout(10 * time.Second) - resp, err := client.R().Get("http://127.0.0.1/nginx_status") - if err != nil || !resp.IsSuccess() { - return h.Success(ctx, []types.NV{}) - } - - raw := resp.String() - var data []types.NV - - workers, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | wc -l") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取负载失败") - } - data = append(data, types.NV{ - Name: "工作进程", - Value: workers, - }) - - out, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取负载失败") - } - mem := str.FormatBytes(cast.ToFloat64(out)) - data = append(data, types.NV{ - Name: "内存占用", - Value: mem, - }) - - match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw) - if len(match) == 2 { - data = append(data, types.NV{ - Name: "活跃连接数", - Value: match[1], - }) - } - - match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw) - if len(match) == 4 { - data = append(data, types.NV{ - Name: "总连接次数", - Value: match[1], - }) - data = append(data, types.NV{ - Name: "总握手次数", - Value: match[2], - }) - data = append(data, types.NV{ - Name: "总请求次数", - Value: match[3], - }) - } - - match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw) - if len(match) == 4 { - data = append(data, types.NV{ - Name: "请求数", - Value: match[1], - }) - data = append(data, types.NV{ - Name: "响应数", - Value: match[2], - }) - data = append(data, types.NV{ - Name: "驻留进程", - Value: match[3], - }) - } - - return h.Success(ctx, data) -} diff --git a/app/plugins/openresty/main.go b/app/plugins/openresty/main.go deleted file mode 100644 index aaa4da0f..00000000 --- a/app/plugins/openresty/main.go +++ /dev/null @@ -1,37 +0,0 @@ -package openresty - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/route" - - "github.com/TheTNB/panel/v2/app/http/middleware" - "github.com/TheTNB/panel/v2/app/plugins/loader" - "github.com/TheTNB/panel/v2/pkg/types" -) - -func init() { - loader.Register(&types.Plugin{ - Name: "OpenResty", - Description: "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台", - Slug: "openresty", - Version: "1.25.3.1", - Requires: []string{}, - Excludes: []string{}, - Install: "bash /www/panel/scripts/openresty/install.sh", - Uninstall: "bash /www/panel/scripts/openresty/uninstall.sh", - Update: "bash /www/panel/scripts/openresty/install.sh", - Boot: func(app foundation.Application) { - RouteFacade := app.MakeRoute() - RouteFacade.Prefix("api/plugins/openresty").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - r.Prefix("openresty").Group(func(route route.Router) { - controller := NewController() - route.Get("load", controller.Load) - route.Get("config", controller.GetConfig) - route.Post("config", controller.SaveConfig) - route.Get("errorLog", controller.ErrorLog) - route.Post("clearErrorLog", controller.ClearErrorLog) - }) - }) - }, - }) -} diff --git a/app/plugins/php_controller.go b/app/plugins/php_controller.go deleted file mode 100644 index f14fd7e5..00000000 --- a/app/plugins/php_controller.go +++ /dev/null @@ -1,246 +0,0 @@ -package plugins - -import ( - "github.com/goravel/framework/contracts/http" - - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" -) - -type PHPController struct{} - -func NewPHPController() *PHPController { - return &PHPController{} -} - -// GetConfig -// -// @Summary 获取配置 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/config [get] -func (r *PHPController) GetConfig(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - config, err := service.GetConfig() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveConfig -// -// @Summary 保存配置 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Param config body string true "配置" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/config [post] -func (r *PHPController) SaveConfig(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - config := ctx.Request().Input("config") - if err := service.SaveConfig(config); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetFPMConfig -// -// @Summary 获取 FPM 配置 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/fpmConfig [get] -func (r *PHPController) GetFPMConfig(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - config, err := service.GetFPMConfig() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveFPMConfig -// -// @Summary 保存 FPM 配置 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Param config body string true "配置" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/fpmConfig [post] -func (r *PHPController) SaveFPMConfig(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - config := ctx.Request().Input("config") - if err := service.SaveFPMConfig(config); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Load -// -// @Summary 获取负载状态 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/load [get] -func (r *PHPController) Load(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - load, err := service.Load() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, load) -} - -// ErrorLog -// -// @Summary 获取错误日志 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/errorLog [get] -func (r *PHPController) ErrorLog(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - log, _ := service.GetErrorLog() - return h.Success(ctx, log) -} - -// SlowLog -// -// @Summary 获取慢日志 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/slowLog [get] -func (r *PHPController) SlowLog(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - log, _ := service.GetSlowLog() - return h.Success(ctx, log) -} - -// ClearErrorLog -// -// @Summary 清空错误日志 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/clearErrorLog [post] -func (r *PHPController) ClearErrorLog(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - err := service.ClearErrorLog() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ClearSlowLog -// -// @Summary 清空慢日志 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/clearSlowLog [post] -func (r *PHPController) ClearSlowLog(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - err := service.ClearSlowLog() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// ExtensionList -// -// @Summary 获取扩展列表 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/extensions [get] -func (r *PHPController) ExtensionList(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - extensions, err := service.GetExtensions() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, extensions) -} - -// InstallExtension -// -// @Summary 安装扩展 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Param slug query string true "slug" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/extensions [post] -func (r *PHPController) InstallExtension(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - slug := ctx.Request().Input("slug") - if len(slug) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - - if err := service.InstallExtension(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// UninstallExtension -// -// @Summary 卸载扩展 -// @Tags 插件-PHP -// @Produce json -// @Security BearerToken -// @Param version path int true "PHP 版本" -// @Param slug query string true "slug" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/php/{version}/extensions [delete] -func (r *PHPController) UninstallExtension(ctx http.Context) http.Response { - service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version"))) - slug := ctx.Request().Input("slug") - if len(slug) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "参数错误") - } - - if err := service.UninstallExtension(slug); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/phpmyadmin_controller.go b/app/plugins/phpmyadmin_controller.go deleted file mode 100644 index d0c386d5..00000000 --- a/app/plugins/phpmyadmin_controller.go +++ /dev/null @@ -1,127 +0,0 @@ -package plugins - -import ( - "fmt" - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type PhpMyAdminController struct { -} - -func NewPhpMyAdminController() *PhpMyAdminController { - return &PhpMyAdminController{} -} - -func (r *PhpMyAdminController) Info(ctx http.Context) http.Response { - files, err := io.ReadDir("/www/server/phpmyadmin") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录") - } - - var phpmyadmin string - for _, f := range files { - if strings.HasPrefix(f.Name(), "phpmyadmin_") { - phpmyadmin = f.Name() - } - } - if len(phpmyadmin) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录") - } - - conf, err := io.Read("/www/server/vhost/phpmyadmin.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf) - if len(match) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 端口") - } - - return h.Success(ctx, http.Json{ - "path": phpmyadmin, - "port": cast.ToInt(match[1]), - }) -} - -func (r *PhpMyAdminController) SetPort(ctx http.Context) http.Response { - port := ctx.Request().InputInt("port") - if port == 0 { - return h.Error(ctx, http.StatusInternalServerError, "端口不能为空") - } - - conf, err := io.Read("/www/server/vhost/phpmyadmin.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(port)+";") - if err := io.Write("/www/server/vhost/phpmyadmin.conf", conf, 0644); err != nil { - facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{ - "error": err.Error(), - }).Info("修改 phpMyAdmin 端口失败") - return h.ErrorSystem(ctx) - } - - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --zone=public --add-port=%d/tcp --permanent", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("ufw allow %d/tcp", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) - } - - return h.Success(ctx, nil) -} - -func (r *PhpMyAdminController) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/www/server/vhost/phpmyadmin.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -func (r *PhpMyAdminController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "配置不能为空") - } - - if err := io.Write("/www/server/vhost/phpmyadmin.conf", config, 0644); err != nil { - facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{ - "error": err.Error(), - }).Info("修改 phpMyAdmin 配置失败") - return h.ErrorSystem(ctx) - } - - if err := systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err)) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/podman_controller.go b/app/plugins/podman_controller.go deleted file mode 100644 index d5bddcef..00000000 --- a/app/plugins/podman_controller.go +++ /dev/null @@ -1,109 +0,0 @@ -package plugins - -import ( - "github.com/goravel/framework/contracts/http" - - requests "github.com/TheTNB/panel/v2/app/http/requests/plugins/podman" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type PodmanController struct { -} - -func NewPodmanController() *PodmanController { - return &PodmanController{} -} - -// GetRegistryConfig -// -// @Summary 获取注册表配置 -// @Description 获取 Podman 注册表配置 -// @Tags 插件-Podman -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/podman/registryConfig [get] -func (r *PodmanController) GetRegistryConfig(ctx http.Context) http.Response { - config, err := io.Read("/etc/containers/registries.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateRegistryConfig -// -// @Summary 更新注册表配置 -// @Description 更新 Podman 注册表配置 -// @Tags 插件-Podman -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateRegistryConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/podman/registryConfig [post] -func (r *PodmanController) UpdateRegistryConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateRegistryConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write("/etc/containers/registries.conf", updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart("podman"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetStorageConfig -// -// @Summary 获取存储配置 -// @Description 获取 Podman 存储配置 -// @Tags 插件-Podman -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/podman/storageConfig [get] -func (r *PodmanController) GetStorageConfig(ctx http.Context) http.Response { - config, err := io.Read("/etc/containers/storage.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateStorageConfig -// -// @Summary 更新存储配置 -// @Description 更新 Podman 存储配置 -// @Tags 插件-Podman -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateStorageConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/podman/storageConfig [post] -func (r *PodmanController) UpdateStorageConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateStorageConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write("/etc/containers/storage.conf", updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart("podman"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/postgresql_controller.go b/app/plugins/postgresql_controller.go deleted file mode 100644 index 55b76c51..00000000 --- a/app/plugins/postgresql_controller.go +++ /dev/null @@ -1,491 +0,0 @@ -package plugins - -import ( - "database/sql" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/support/carbon" - _ "github.com/lib/pq" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PostgreSQLController struct { - setting internal.Setting - backup internal.Backup -} - -func NewPostgreSQLController() *PostgreSQLController { - return &PostgreSQLController{ - setting: services.NewSettingImpl(), - backup: services.NewBackupImpl(), - } -} - -// GetConfig 获取配置 -func (r *PostgreSQLController) GetConfig(ctx http.Context) http.Response { - // 获取配置 - config, err := io.Read("/www/server/postgresql/data/postgresql.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败") - } - - return h.Success(ctx, config) -} - -// GetUserConfig 获取用户配置 -func (r *PostgreSQLController) GetUserConfig(ctx http.Context) http.Response { - // 获取配置 - config, err := io.Read("/www/server/postgresql/data/pg_hba.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败") - } - - return h.Success(ctx, config) -} - -// SaveConfig 保存配置 -func (r *PostgreSQLController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空") - } - - if err := io.Write("/www/server/postgresql/data/postgresql.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败") - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// SaveUserConfig 保存用户配置 -func (r *PostgreSQLController) SaveUserConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空") - } - - if err := io.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败") - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// Load 获取负载 -func (r *PostgreSQLController) Load(ctx http.Context) http.Response { - status, _ := systemctl.Status("postgresql") - if !status { - return h.Success(ctx, []types.NV{}) - } - - time, err := shell.Execf(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL启动时间失败") - } - pid, err := shell.Execf(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程PID失败") - } - process, err := shell.Execf(`ps aux | grep postgres | grep -v grep | wc -l`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程数失败") - } - connections, err := shell.Execf(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL连接数失败") - } - storage, err := shell.Execf(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败") - } - - data := []types.NV{ - {Name: "启动时间", Value: carbon.Parse(time).ToDateTimeString()}, - {Name: "进程 PID", Value: pid}, - {Name: "进程数", Value: process}, - {Name: "总连接数", Value: connections}, - {Name: "空间占用", Value: storage}, - } - - return h.Success(ctx, data) -} - -// Log 获取日志 -func (r *PostgreSQLController) Log(ctx http.Context) http.Response { - log, err := shell.Execf("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearLog 清空日志 -func (r *PostgreSQLController) ClearLog(ctx http.Context) http.Response { - if out, err := shell.Execf("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// DatabaseList 获取数据库列表 -func (r *PostgreSQLController) DatabaseList(ctx http.Context) http.Response { - type database struct { - Name string `json:"name"` - Owner string `json:"owner"` - Encoding string `json:"encoding"` - } - - db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=postgres sslmode=disable") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []database{}, - }) - } - - if err = db.Ping(); err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []database{}, - }) - } - - query := ` - SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba), pg_catalog.pg_encoding_to_char(d.encoding) - FROM pg_catalog.pg_database d - WHERE datistemplate = false; - ` - rows, err := db.Query(query) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - defer rows.Close() - - var databases []database - for rows.Next() { - var db database - if err := rows.Scan(&db.Name, &db.Owner, &db.Encoding); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - databases = append(databases, db) - } - if err = rows.Err(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, databases) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddDatabase 添加数据库 -func (r *PostgreSQLController) AddDatabase(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$", - "user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:40", - }); sanitize != nil { - return sanitize - } - - database := ctx.Request().Input("database") - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - - if out, err := shell.Execf(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256" - if out, err := shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// DeleteDatabase 删除数据库 -func (r *PostgreSQLController) DeleteDatabase(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1", - }); sanitize != nil { - return sanitize - } - - database := ctx.Request().Input("database") - if out, err := shell.Execf(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// BackupList 获取备份列表 -func (r *PostgreSQLController) BackupList(ctx http.Context) http.Response { - backups, err := r.backup.PostgresqlList() - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取备份列表失败") - } - - paged, total := h.Paginate(ctx, backups) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// UploadBackup 上传备份 -func (r *PostgreSQLController) UploadBackup(ctx http.Context) http.Response { - file, err := ctx.Request().File("file") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败") - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql" - if !io.Exists(backupPath) { - if err = io.Mkdir(backupPath, 0644); err != nil { - return nil - } - } - - name := file.GetClientOriginalName() - _, err = file.StoreAs(backupPath, name) - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败") - } - - return h.Success(ctx, nil) -} - -// CreateBackup 创建备份 -func (r *PostgreSQLController) CreateBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1", - }); sanitize != nil { - return sanitize - } - - database := ctx.Request().Input("database") - if err := r.backup.PostgresqlBackup(database); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// DeleteBackup 删除备份 -func (r *PostgreSQLController) DeleteBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|min_len:1|max_len:255", - }); sanitize != nil { - return sanitize - } - - backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql" - fileName := ctx.Request().Input("name") - if err := io.Remove(backupPath + "/" + fileName); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// RestoreBackup 还原备份 -func (r *PostgreSQLController) RestoreBackup(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "backup": "required|min_len:1|max_len:255", - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1", - }); sanitize != nil { - return sanitize - } - - if err := r.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error()) - } - - return h.Success(ctx, nil) -} - -// RoleList 角色列表 -func (r *PostgreSQLController) RoleList(ctx http.Context) http.Response { - type role struct { - Role string `json:"role"` - Attributes []string `json:"attributes"` - } - - db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=postgres sslmode=disable") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []role{}, - }) - } - if err = db.Ping(); err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []role{}, - }) - } - - query := ` - SELECT rolname, - rolsuper, - rolcreaterole, - rolcreatedb, - rolreplication, - rolbypassrls - FROM pg_roles - WHERE rolcanlogin = true; - ` - rows, err := db.Query(query) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - defer rows.Close() - - var roles []role - for rows.Next() { - var r role - var super, canCreateRole, canCreateDb, replication, bypassRls bool - if err = rows.Scan(&r.Role, &super, &canCreateRole, &canCreateDb, &replication, &bypassRls); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - permissions := map[string]bool{ - "超级用户": super, - "创建角色": canCreateRole, - "创建数据库": canCreateDb, - "可以复制": replication, - "绕过行级安全": bypassRls, - } - for perm, enabled := range permissions { - if enabled { - r.Attributes = append(r.Attributes, perm) - } - } - - if len(r.Attributes) == 0 { - r.Attributes = append(r.Attributes, "无") - } - - roles = append(roles, r) - } - if err = rows.Err(); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - paged, total := h.Paginate(ctx, roles) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// AddRole 添加角色 -func (r *PostgreSQLController) AddRole(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$", - "user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:40", - }); sanitize != nil { - return sanitize - } - - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - database := ctx.Request().Input("database") - if out, err := shell.Execf(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256" - if out, err := shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// DeleteRole 删除角色 -func (r *PostgreSQLController) DeleteRole(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$", - }); sanitize != nil { - return sanitize - } - - user := ctx.Request().Input("user") - if out, err := shell.Execf(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := systemctl.Reload("postgresql"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重载服务失败") - } - - return h.Success(ctx, nil) -} - -// SetRolePassword 设置用户密码 -func (r *PostgreSQLController) SetRolePassword(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$", - "password": "required|min_len:8|max_len:40", - }); sanitize != nil { - return sanitize - } - - user := ctx.Request().Input("user") - password := ctx.Request().Input("password") - if out, err := shell.Execf(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/pureftpd_controller.go b/app/plugins/pureftpd_controller.go deleted file mode 100644 index 79394c5d..00000000 --- a/app/plugins/pureftpd_controller.go +++ /dev/null @@ -1,179 +0,0 @@ -package plugins - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PureFtpdController struct { -} - -func NewPureFtpdController() *PureFtpdController { - return &PureFtpdController{} -} - -// List 获取用户列表 -func (r *PureFtpdController) List(ctx http.Context) http.Response { - listRaw, err := shell.Execf("pure-pw list") - if err != nil { - return h.Success(ctx, http.Json{ - "total": 0, - "items": []types.PureFtpdUser{}, - }) - } - - listArr := strings.Split(listRaw, "\n") - var users []types.PureFtpdUser - for _, v := range listArr { - if len(v) == 0 { - continue - } - - match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v) - users = append(users, types.PureFtpdUser{ - Username: match[1], - Path: strings.Replace(match[2], "/./", "/", 1), - }) - } - - paged, total := h.Paginate(ctx, users) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Add 添加用户 -func (r *PureFtpdController) Add(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "username": "required", - "password": "required|min_len:6", - "path": "required", - }); sanitize != nil { - return sanitize - } - - username := ctx.Request().Input("username") - password := ctx.Request().Input("password") - path := ctx.Request().Input("path") - - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - if !io.Exists(path) { - return h.Error(ctx, http.StatusUnprocessableEntity, "目录不存在") - } - - if err := io.Chmod(path, 0755); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "修改目录权限失败") - } - if err := io.Chown(path, "www", "www"); err != nil { - return nil - } - if out, err := shell.Execf(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("pure-pw mkdb"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// Delete 删除用户 -func (r *PureFtpdController) Delete(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "username": "required", - }); sanitize != nil { - return sanitize - } - - username := ctx.Request().Input("username") - - if out, err := shell.Execf("pure-pw userdel " + username + " -m"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("pure-pw mkdb"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// ChangePassword 修改密码 -func (r *PureFtpdController) ChangePassword(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "username": "required", - "password": "required|min_len:6", - }); sanitize != nil { - return sanitize - } - - username := ctx.Request().Input("username") - password := ctx.Request().Input("password") - - if out, err := shell.Execf(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("pure-pw mkdb"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// GetPort 获取端口 -func (r *PureFtpdController) GetPort(ctx http.Context) http.Response { - port, err := shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败") - } - - return h.Success(ctx, cast.ToInt(port)) -} - -// SetPort 设置端口 -func (r *PureFtpdController) SetPort(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "port": "required", - }); sanitize != nil { - return sanitize - } - - port := ctx.Request().Input("port") - if out, err := shell.Execf(`sed -i "s/Bind.*/Bind 0.0.0.0,%s/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`, port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if os.IsRHEL() { - if out, err := shell.Execf("firewall-cmd --zone=public --add-port=%s/tcp --permanent", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("firewall-cmd --reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } else { - if out, err := shell.Execf("ufw allow %s/tcp", port); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf("ufw reload"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if err := systemctl.Restart("pure-ftpd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/redis_controller.go b/app/plugins/redis_controller.go deleted file mode 100644 index 1e234182..00000000 --- a/app/plugins/redis_controller.go +++ /dev/null @@ -1,93 +0,0 @@ -package plugins - -import ( - "strings" - - "github.com/goravel/framework/contracts/http" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type RedisController struct { -} - -func NewRedisController() *RedisController { - return &RedisController{} -} - -// GetConfig 获取配置 -func (r *RedisController) GetConfig(ctx http.Context) http.Response { - // 获取配置 - config, err := io.Read("/www/server/redis/redis.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取Redis配置失败") - } - - return h.Success(ctx, config) -} - -// SaveConfig 保存配置 -func (r *RedisController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - if len(config) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空") - } - - if err := io.Write("/www/server/redis/redis.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入Redis配置失败") - } - - if err := systemctl.Restart("redis"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "重启Redis失败") - } - - return h.Success(ctx, nil) -} - -// Load 获取负载 -func (r *RedisController) Load(ctx http.Context) http.Response { - status, err := systemctl.Status("redis") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败") - } - if !status { - return h.Error(ctx, http.StatusInternalServerError, "Redis已停止运行") - } - - raw, err := shell.Execf("redis-cli info") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取Redis负载失败") - } - - infoLines := strings.Split(raw, "\n") - dataRaw := make(map[string]string) - - for _, item := range infoLines { - parts := strings.Split(item, ":") - if len(parts) == 2 { - dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - } - - data := []types.NV{ - {Name: "TCP 端口", Value: dataRaw["tcp_port"]}, - {Name: "已运行天数", Value: dataRaw["uptime_in_days"]}, - {Name: "连接的客户端数", Value: dataRaw["connected_clients"]}, - {Name: "已分配的内存总量", Value: dataRaw["used_memory_human"]}, - {Name: "占用内存总量", Value: dataRaw["used_memory_rss_human"]}, - {Name: "占用内存峰值", Value: dataRaw["used_memory_peak_human"]}, - {Name: "内存碎片比率", Value: dataRaw["mem_fragmentation_ratio"]}, - {Name: "运行以来连接过的客户端的总数", Value: dataRaw["total_connections_received"]}, - {Name: "运行以来执行过的命令的总数", Value: dataRaw["total_commands_processed"]}, - {Name: "每秒执行的命令数", Value: dataRaw["instantaneous_ops_per_sec"]}, - {Name: "查找数据库键成功次数", Value: dataRaw["keyspace_hits"]}, - {Name: "查找数据库键失败次数", Value: dataRaw["keyspace_misses"]}, - {Name: "最近一次 fork() 操作耗费的毫秒数", Value: dataRaw["latest_fork_usec"]}, - } - - return h.Success(ctx, data) -} diff --git a/app/plugins/rsync_controller.go b/app/plugins/rsync_controller.go deleted file mode 100644 index 1c15b859..00000000 --- a/app/plugins/rsync_controller.go +++ /dev/null @@ -1,299 +0,0 @@ -package plugins - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - - requests "github.com/TheTNB/panel/v2/app/http/requests/plugins/rsync" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type RsyncController struct { -} - -func NewRsyncController() *RsyncController { - return &RsyncController{} -} - -// List -// -// @Summary 列出模块 -// @Description 列出所有 Rsync 模块 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param data query commonrequests.Paginate true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/modules [get] -func (r *RsyncController) List(ctx http.Context) http.Response { - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - var modules []types.RsyncModule - lines := strings.Split(config, "\n") - var currentModule *types.RsyncModule - - for _, line := range lines { - line = strings.TrimSpace(line) - - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { - if currentModule != nil { - modules = append(modules, *currentModule) - } - moduleName := line[1 : len(line)-1] - currentModule = &types.RsyncModule{ - Name: moduleName, - } - } else if currentModule != nil { - parts := strings.SplitN(line, "=", 2) - if len(parts) == 2 { - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - switch key { - case "path": - currentModule.Path = value - case "comment": - currentModule.Comment = value - case "read only": - currentModule.ReadOnly = value == "yes" || value == "true" - case "auth users": - currentModule.AuthUser = value - currentModule.Secret, err = shell.Execf("grep -E '^" + currentModule.AuthUser + ":.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取模块"+currentModule.AuthUser+"的密钥失败") - } - case "hosts allow": - currentModule.HostsAllow = value - } - } - } - } - - if currentModule != nil { - modules = append(modules, *currentModule) - } - - paged, total := h.Paginate(ctx, modules) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Create -// -// @Summary 添加模块 -// @Description 添加 Rsync 模块 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param data body requests.Create true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/modules [post] -func (r *RsyncController) Create(ctx http.Context) http.Response { - var createRequest requests.Create - sanitize := h.SanitizeRequest(ctx, &createRequest) - if sanitize != nil { - return sanitize - } - - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if strings.Contains(config, "["+createRequest.Name+"]") { - return h.Error(ctx, http.StatusUnprocessableEntity, "模块 "+createRequest.Name+" 已存在") - } - - conf := `# ` + createRequest.Name + `-START -[` + createRequest.Name + `] -path = ` + createRequest.Path + ` -comment = ` + createRequest.Comment + ` -read only = no -auth users = ` + createRequest.AuthUser + ` -hosts allow = ` + createRequest.HostsAllow + ` -secrets file = /etc/rsyncd.secrets -# ` + createRequest.Name + `-END -` - - if err := io.WriteAppend("/etc/rsyncd.conf", conf); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if out, err := shell.Execf("echo '" + createRequest.AuthUser + ":" + createRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err := systemctl.Restart("rsyncd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Destroy -// -// @Summary 删除模块 -// @Description 删除 Rsync 模块 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param name path string true "模块名称" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/modules/{name} [delete] -func (r *RsyncController) Destroy(ctx http.Context) http.Response { - name := ctx.Request().Input("name") - if len(name) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "name 不能为空") - } - - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if !strings.Contains(config, "["+name+"]") { - return h.Error(ctx, http.StatusUnprocessableEntity, "模块 "+name+" 不存在") - } - - module := str.Cut(config, "# "+name+"-START", "# "+name+"-END") - config = strings.Replace(config, "\n# "+name+"-START"+module+"# "+name+"-END", "", -1) - - match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module) - if len(match) == 2 { - authUser := match[1] - if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = systemctl.Restart("rsyncd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// Update -// -// @Summary 更新模块 -// @Description 更新 Rsync 模块 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param name path string true "模块名称" -// @Param data body requests.Update true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/modules/{name} [post] -func (r *RsyncController) Update(ctx http.Context) http.Response { - var updateRequest requests.Update - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if !strings.Contains(config, "["+updateRequest.Name+"]") { - return h.Error(ctx, http.StatusUnprocessableEntity, "模块 "+updateRequest.Name+" 不存在") - } - - newConf := `# ` + updateRequest.Name + `-START -[` + updateRequest.Name + `] -path = ` + updateRequest.Path + ` -comment = ` + updateRequest.Comment + ` -read only = no -auth users = ` + updateRequest.AuthUser + ` -hosts allow = ` + updateRequest.HostsAllow + ` -secrets file = /etc/rsyncd.secrets -# ` + updateRequest.Name + `-END` - - module := str.Cut(config, "# "+updateRequest.Name+"-START", "# "+updateRequest.Name+"-END") - config = strings.Replace(config, "# "+updateRequest.Name+"-START"+module+"# "+updateRequest.Name+"-END", newConf, -1) - - match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module) - if len(match) == 2 { - authUser := match[1] - if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - } - - if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - if out, err := shell.Execf("echo '" + updateRequest.AuthUser + ":" + updateRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - if err = systemctl.Restart("rsyncd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} - -// GetConfig -// -// @Summary 获取配置 -// @Description 获取 Rsync 配置 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/config [get] -func (r *RsyncController) GetConfig(ctx http.Context) http.Response { - config, err := io.Read("/etc/rsyncd.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// UpdateConfig -// -// @Summary 更新配置 -// @Description 更新 Rsync 配置 -// @Tags 插件-Rsync -// @Produce json -// @Security BearerToken -// @Param data body requests.UpdateConfig true "request" -// @Success 200 {object} controllers.SuccessResponse -// @Router /plugins/rsync/config [post] -func (r *RsyncController) UpdateConfig(ctx http.Context) http.Response { - var updateRequest requests.UpdateConfig - sanitize := h.SanitizeRequest(ctx, &updateRequest) - if sanitize != nil { - return sanitize - } - - if err := io.Write("/etc/rsyncd.conf", updateRequest.Config, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err := systemctl.Restart("rsyncd"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/s3fs_controller.go b/app/plugins/s3fs_controller.go deleted file mode 100644 index c16c1b3e..00000000 --- a/app/plugins/s3fs_controller.go +++ /dev/null @@ -1,181 +0,0 @@ -package plugins - -import ( - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/json" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type S3fsController struct { - setting internal.Setting -} - -func NewS3fsController() *S3fsController { - return &S3fsController{ - setting: services.NewSettingImpl(), - } -} - -// List 所有 S3fs 挂载 -func (r *S3fsController) List(ctx http.Context) http.Response { - var s3fsList []types.S3fsMount - err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败") - } - - paged, total := h.Paginate(ctx, s3fsList) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// Add 添加 S3fs 挂载 -func (r *S3fsController) Add(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "ak": "required|regex:^[a-zA-Z0-9]*$", - "sk": "required|regex:^[a-zA-Z0-9]*$", - "bucket": "required|regex:^[a-zA-Z0-9_-]*$", - "url": "required|full_url", - "path": "required|regex:^/[a-zA-Z0-9_-]+$", - }); sanitize != nil { - return sanitize - } - - ak := ctx.Request().Input("ak") - sk := ctx.Request().Input("sk") - path := ctx.Request().Input("path") - bucket := ctx.Request().Input("bucket") - url := ctx.Request().Input("url") - - // 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的 - if strings.Contains(url, bucket) { - return h.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称") - } - - // 检查挂载目录是否存在且为空 - if !io.Exists(path) { - if err := io.Mkdir(path, 0755); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "挂载目录创建失败") - } - } - if !io.Empty(path) { - return h.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空") - } - - var s3fsList []types.S3fsMount - if err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败") - } - - for _, s := range s3fsList { - if s.Path == path { - return h.Error(ctx, http.StatusUnprocessableEntity, "路径已存在") - } - } - - id := carbon.Now().TimestampMilli() - password := ak + ":" + sk - if err := io.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600); err != nil { - return nil - } - out, err := shell.Execf(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil { - _, _ = shell.Execf(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`) - return h.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck) - } - if _, err := shell.Execf("df -h | grep " + path + " 2>&1"); err != nil { - _, _ = shell.Execf(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`) - return h.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确") - } - - s3fsList = append(s3fsList, types.S3fsMount{ - ID: id, - Path: path, - Bucket: bucket, - Url: url, - }) - encoded, err := json.MarshalString(s3fsList) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败") - } - err = r.setting.Set("s3fs", encoded) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败") - } - - return h.Success(ctx, nil) -} - -// Delete 删除 S3fs 挂载 -func (r *S3fsController) Delete(ctx http.Context) http.Response { - id := ctx.Request().Input("id") - if len(id) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空") - } - - var s3fsList []types.S3fsMount - err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败") - } - - var mount types.S3fsMount - for _, s := range s3fsList { - if cast.ToString(s.ID) == id { - mount = s - break - } - } - if mount.ID == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在") - } - - if out, err := shell.Execf(`fusermount -u '` + mount.Path + `' 2>&1`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`umount '` + mount.Path + `' 2>&1`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if out, err := shell.Execf(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck) - } - if err := io.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID)); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - var newS3fsList []types.S3fsMount - for _, s := range s3fsList { - if s.ID != mount.ID { - newS3fsList = append(newS3fsList, s) - } - } - encoded, err := json.MarshalString(newS3fsList) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败") - } - err = r.setting.Set("s3fs", encoded) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败") - } - - return h.Success(ctx, nil) -} diff --git a/app/plugins/supervisor_controller.go b/app/plugins/supervisor_controller.go deleted file mode 100644 index 4be32032..00000000 --- a/app/plugins/supervisor_controller.go +++ /dev/null @@ -1,336 +0,0 @@ -package plugins - -import ( - "fmt" - "strconv" - "strings" - - "github.com/goravel/framework/contracts/http" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type SupervisorController struct { - service string -} - -func NewSupervisorController() *SupervisorController { - var service string - if os.IsRHEL() { - service = "supervisord" - } else { - service = "supervisor" - } - - return &SupervisorController{ - service: service, - } -} - -// Service 获取服务名称 -func (r *SupervisorController) Service(ctx http.Context) http.Response { - return h.Success(ctx, r.service) -} - -// Log 日志 -func (r *SupervisorController) Log(ctx http.Context) http.Response { - log, err := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearLog 清空日志 -func (r *SupervisorController) ClearLog(ctx http.Context) http.Response { - if out, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// Config 获取配置 -func (r *SupervisorController) Config(ctx http.Context) http.Response { - var config string - var err error - if os.IsRHEL() { - config, err = io.Read(`/etc/supervisord.conf`) - } else { - config, err = io.Read(`/etc/supervisor/supervisord.conf`) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveConfig 保存配置 -func (r *SupervisorController) SaveConfig(ctx http.Context) http.Response { - config := ctx.Request().Input("config") - var err error - if os.IsRHEL() { - err = io.Write(`/etc/supervisord.conf`, config, 0644) - } else { - err = io.Write(`/etc/supervisor/supervisord.conf`, config, 0644) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - if err = systemctl.Restart(r.service); err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", r.service)) - } - - return h.Success(ctx, nil) -} - -// Processes 进程列表 -func (r *SupervisorController) Processes(ctx http.Context) http.Response { - type process struct { - Name string `json:"name"` - Status string `json:"status"` - Pid string `json:"pid"` - Uptime string `json:"uptime"` - } - - out, err := shell.Execf(`supervisorctl status | awk '{print $1}'`) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - var processes []process - for _, line := range strings.Split(out, "\n") { - if len(line) == 0 { - continue - } - - var p process - p.Name = line - if status, err := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $2}'`); err == nil { - p.Status = status - } - if p.Status == "RUNNING" { - pid, _ := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $4}'`) - p.Pid = strings.ReplaceAll(pid, ",", "") - uptime, _ := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $6}'`) - p.Uptime = uptime - } else { - p.Pid = "-" - p.Uptime = "-" - } - processes = append(processes, p) - } - - paged, total := h.Paginate(ctx, processes) - - return h.Success(ctx, http.Json{ - "total": total, - "items": paged, - }) -} - -// StartProcess 启动进程 -func (r *SupervisorController) StartProcess(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - if out, err := shell.Execf(`supervisorctl start %s`, process); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// StopProcess 停止进程 -func (r *SupervisorController) StopProcess(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - if out, err := shell.Execf(`supervisorctl stop %s`, process); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// RestartProcess 重启进程 -func (r *SupervisorController) RestartProcess(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - if out, err := shell.Execf(`supervisorctl restart %s`, process); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// ProcessLog 进程日志 -func (r *SupervisorController) ProcessLog(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - var logPath string - var err error - if os.IsRHEL() { - logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - } else { - logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径") - } - - log, err := shell.Execf(`tail -n 200 ` + logPath) - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, log) - } - - return h.Success(ctx, log) -} - -// ClearProcessLog 清空进程日志 -func (r *SupervisorController) ClearProcessLog(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - var logPath string - var err error - if os.IsRHEL() { - logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - } else { - logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("无法从进程%s的配置文件中获取日志路径", process)) - } - - if out, err := shell.Execf(`echo "" > ` + logPath); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - return h.Success(ctx, nil) -} - -// ProcessConfig 获取进程配置 -func (r *SupervisorController) ProcessConfig(ctx http.Context) http.Response { - process := ctx.Request().Query("process") - var config string - var err error - if os.IsRHEL() { - config, err = io.Read(`/etc/supervisord.d/` + process + `.conf`) - } else { - config, err = io.Read(`/etc/supervisor/conf.d/` + process + `.conf`) - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - - return h.Success(ctx, config) -} - -// SaveProcessConfig 保存进程配置 -func (r *SupervisorController) SaveProcessConfig(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - config := ctx.Request().Input("config") - var err error - if os.IsRHEL() { - err = io.Write(`/etc/supervisord.d/`+process+`.conf`, config, 0644) - } else { - err = io.Write(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644) - } - - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - - _, _ = shell.Execf(`supervisorctl reread`) - _, _ = shell.Execf(`supervisorctl update`) - _, _ = shell.Execf(`supervisorctl restart %s`, process) - - return h.Success(ctx, nil) -} - -// AddProcess 添加进程 -func (r *SupervisorController) AddProcess(ctx http.Context) http.Response { - if sanitize := h.Sanitize(ctx, map[string]string{ - "name": "required|alpha_dash", - "user": "required|alpha_dash", - "path": "required", - "command": "required", - "num": "required", - }); sanitize != nil { - return sanitize - } - - name := ctx.Request().Input("name") - user := ctx.Request().Input("user") - path := ctx.Request().Input("path") - command := ctx.Request().Input("command") - num := ctx.Request().InputInt("num", 1) - config := `[program:` + name + `] -command=` + command + ` -process_name=%(program_name)s -directory=` + path + ` -autostart=true -autorestart=true -user=` + user + ` -numprocs=` + strconv.Itoa(num) + ` -redirect_stderr=true -stdout_logfile=/var/log/supervisor/` + name + `.log -stdout_logfile_maxbytes=2MB -` - - var err error - if os.IsRHEL() { - err = io.Write(`/etc/supervisord.d/`+name+`.conf`, config, 0644) - } else { - err = io.Write(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644) - } - - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - - _, _ = shell.Execf(`supervisorctl reread`) - _, _ = shell.Execf(`supervisorctl update`) - _, _ = shell.Execf(`supervisorctl start %s`, name) - - return h.Success(ctx, nil) -} - -// DeleteProcess 删除进程 -func (r *SupervisorController) DeleteProcess(ctx http.Context) http.Response { - process := ctx.Request().Input("process") - if out, err := shell.Execf(`supervisorctl stop %s`, process); err != nil { - return h.Error(ctx, http.StatusInternalServerError, out) - } - - var logPath string - var err error - if os.IsRHEL() { - logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - if err := io.Remove(`/etc/supervisord.d/` + process + `.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } else { - logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process) - if err := io.Remove(`/etc/supervisor/conf.d/` + process + `.conf`); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - } - - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径") - } - - if err := io.Remove(logPath); err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - _, _ = shell.Execf(`supervisorctl reread`) - _, _ = shell.Execf(`supervisorctl update`) - - return h.Success(ctx, nil) -} diff --git a/app/plugins/toolbox_controller.go b/app/plugins/toolbox_controller.go deleted file mode 100644 index a6e079fc..00000000 --- a/app/plugins/toolbox_controller.go +++ /dev/null @@ -1,241 +0,0 @@ -package plugins - -import ( - "regexp" - "strings" - - "github.com/goravel/framework/contracts/http" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/h" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type ToolBoxController struct { -} - -func NewToolBoxController() *ToolBoxController { - return &ToolBoxController{} -} - -// GetDNS 获取 DNS 信息 -func (r *ToolBoxController) GetDNS(ctx http.Context) http.Response { - raw, err := io.Read("/etc/resolv.conf") - if err != nil { - return h.Error(ctx, http.StatusInternalServerError, err.Error()) - } - match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1) - if len(match) == 0 { - return h.Error(ctx, http.StatusInternalServerError, "找不到 DNS 信息") - } - - var dns []string - for _, m := range match { - dns = append(dns, m[1]) - } - - return h.Success(ctx, dns) -} - -// SetDNS 设置 DNS 信息 -func (r *ToolBoxController) SetDNS(ctx http.Context) http.Response { - dns1 := ctx.Request().Input("dns1") - dns2 := ctx.Request().Input("dns2") - - if len(dns1) == 0 || len(dns2) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "DNS 信息不能为空") - } - - var dns string - dns += "nameserver " + dns1 + "\n" - dns += "nameserver " + dns2 + "\n" - - if err := io.Write("/etc/resolv.conf", dns, 0644); err != nil { - return h.Error(ctx, http.StatusInternalServerError, "写入 DNS 信息失败") - } - - return h.Success(ctx, nil) -} - -// GetSWAP 获取 SWAP 信息 -func (r *ToolBoxController) GetSWAP(ctx http.Context) http.Response { - var total, used, free string - var size int64 - if io.Exists("/www/swap") { - file, err := io.FileInfo("/www/swap") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败") - } - - size = file.Size() / 1024 / 1024 - total = str.FormatBytes(float64(file.Size())) - } else { - size = 0 - total = "0.00 B" - } - - raw, err := shell.Execf("free | grep Swap") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败") - } - - match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw) - if len(match) > 0 { - used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024) - free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024) - } - - return h.Success(ctx, http.Json{ - "total": total, - "size": size, - "used": used, - "free": free, - }) -} - -// SetSWAP 设置 SWAP 信息 -func (r *ToolBoxController) SetSWAP(ctx http.Context) http.Response { - size := ctx.Request().InputInt("size") - - if io.Exists("/www/swap") { - if out, err := shell.Execf("swapoff /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if out, err := shell.Execf("rm -f /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if out, err := shell.Execf("sed -i '/www\\/swap/d' /etc/fstab"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - } - - if size > 1 { - free, err := shell.Execf("df -k /www | awk '{print $4}' | tail -n 1") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取磁盘空间失败") - } - if cast.ToInt64(free)*1024 < int64(size)*1024*1024 { - return h.Error(ctx, http.StatusUnprocessableEntity, "磁盘空间不足,当前剩余 "+str.FormatBytes(cast.ToFloat64(free))) - } - - btrfsCheck, _ := shell.Execf("df -T /www | awk '{print $2}' | tail -n 1") - if strings.Contains(btrfsCheck, "btrfs") { - if out, err := shell.Execf("btrfs filesystem mkswapfile --size " + cast.ToString(size) + "M --uuid clear /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - } else { - if out, err := shell.Execf("dd if=/dev/zero of=/www/swap bs=1M count=" + cast.ToString(size)); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if out, err := shell.Execf("mkswap -f /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if err := io.Chmod("/www/swap", 0600); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "设置 SWAP 权限失败") - } - } - if out, err := shell.Execf("swapon /www/swap"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - if out, err := shell.Execf("echo '/www/swap swap swap defaults 0 0' >> /etc/fstab"); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - } - - return h.Success(ctx, nil) -} - -// GetTimezone 获取时区 -func (r *ToolBoxController) GetTimezone(ctx http.Context) http.Response { - raw, err := shell.Execf("timedatectl | grep zone") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取时区信息失败") - } - - match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw) - if len(match) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "找不到时区信息") - } - - type zone struct { - Label string `json:"label"` - Value string `json:"value"` - } - - zonesRaw, err := shell.Execf("timedatectl list-timezones") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "获取时区列表失败") - } - zones := strings.Split(zonesRaw, "\n") - - var zonesList []zone - for _, z := range zones { - zonesList = append(zonesList, zone{ - Label: z, - Value: z, - }) - } - - return h.Success(ctx, http.Json{ - "timezone": match[1], - "timezones": zonesList, - }) -} - -// SetTimezone 设置时区 -func (r *ToolBoxController) SetTimezone(ctx http.Context) http.Response { - timezone := ctx.Request().Input("timezone") - if len(timezone) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "时区不能为空") - } - - if out, err := shell.Execf("timedatectl set-timezone %s", timezone); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - - return h.Success(ctx, nil) -} - -// GetHosts 获取 hosts 信息 -func (r *ToolBoxController) GetHosts(ctx http.Context) http.Response { - hosts, err := io.Read("/etc/hosts") - if err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - - return h.Success(ctx, hosts) -} - -// SetHosts 设置 hosts 信息 -func (r *ToolBoxController) SetHosts(ctx http.Context) http.Response { - hosts := ctx.Request().Input("hosts") - if len(hosts) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "hosts 信息不能为空") - } - - if err := io.Write("/etc/hosts", hosts, 0644); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, "写入 hosts 信息失败") - } - - return h.Success(ctx, nil) -} - -// SetRootPassword 设置 root 密码 -func (r *ToolBoxController) SetRootPassword(ctx http.Context) http.Response { - password := ctx.Request().Input("password") - if len(password) == 0 { - return h.Error(ctx, http.StatusUnprocessableEntity, "密码不能为空") - } - if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(password) { - return h.Error(ctx, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符") - } - - password = strings.ReplaceAll(password, `'`, `\'`) - if out, err := shell.Execf(`yes '` + password + `' | passwd root`); err != nil { - return h.Error(ctx, http.StatusUnprocessableEntity, out) - } - - return h.Success(ctx, nil) -} diff --git a/app/providers/app_service_provider.go b/app/providers/app_service_provider.go deleted file mode 100644 index a4dd934d..00000000 --- a/app/providers/app_service_provider.go +++ /dev/null @@ -1,16 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" -) - -type AppServiceProvider struct { -} - -func (receiver *AppServiceProvider) Register(app foundation.Application) { - -} - -func (receiver *AppServiceProvider) Boot(app foundation.Application) { - -} diff --git a/app/providers/auth_service_provider.go b/app/providers/auth_service_provider.go deleted file mode 100644 index 6bdc039e..00000000 --- a/app/providers/auth_service_provider.go +++ /dev/null @@ -1,16 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" -) - -type AuthServiceProvider struct { -} - -func (receiver *AuthServiceProvider) Register(app foundation.Application) { - -} - -func (receiver *AuthServiceProvider) Boot(app foundation.Application) { - -} diff --git a/app/providers/console_service_provider.go b/app/providers/console_service_provider.go deleted file mode 100644 index 18d5090d..00000000 --- a/app/providers/console_service_provider.go +++ /dev/null @@ -1,21 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/console" -) - -type ConsoleServiceProvider struct { -} - -func (receiver *ConsoleServiceProvider) Register(app foundation.Application) { - kernel := console.Kernel{} - facades.Schedule().Register(kernel.Schedule()) - facades.Artisan().Register(kernel.Commands()) -} - -func (receiver *ConsoleServiceProvider) Boot(app foundation.Application) { - -} diff --git a/app/providers/database_service_provider.go b/app/providers/database_service_provider.go deleted file mode 100644 index 7a08070a..00000000 --- a/app/providers/database_service_provider.go +++ /dev/null @@ -1,22 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/database/seeder" - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/database/gorm" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/pkg/migrate" -) - -type DatabaseServiceProvider struct { -} - -func (receiver *DatabaseServiceProvider) Register(app foundation.Application) { - -} - -func (receiver *DatabaseServiceProvider) Boot(app foundation.Application) { - facades.Seeder().Register([]seeder.Seeder{}) - migrate.Migrate(facades.Orm().Query().(*gorm.QueryImpl).Instance()) -} diff --git a/app/providers/event_service_provider.go b/app/providers/event_service_provider.go deleted file mode 100644 index 24e66f81..00000000 --- a/app/providers/event_service_provider.go +++ /dev/null @@ -1,21 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/event" - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/facades" -) - -type EventServiceProvider struct{} - -func (receiver *EventServiceProvider) Register(app foundation.Application) { - facades.Event().Register(receiver.listen()) -} - -func (receiver *EventServiceProvider) Boot(app foundation.Application) { - -} - -func (receiver *EventServiceProvider) listen() map[event.Event][]event.Listener { - return map[event.Event][]event.Listener{} -} diff --git a/app/providers/plugin_service_provider.go b/app/providers/plugin_service_provider.go deleted file mode 100644 index 43858399..00000000 --- a/app/providers/plugin_service_provider.go +++ /dev/null @@ -1,20 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - - "github.com/TheTNB/panel/v2/app/plugins" - "github.com/TheTNB/panel/v2/app/plugins/loader" -) - -type PluginServiceProvider struct{} - -func (receiver *PluginServiceProvider) Register(app foundation.Application) { - plugins.Boot() -} - -func (receiver *PluginServiceProvider) Boot(app foundation.Application) { - for _, plugin := range loader.All() { - plugin.Boot(app) - } -} diff --git a/app/providers/queue_service_provider.go b/app/providers/queue_service_provider.go deleted file mode 100644 index cc8f1215..00000000 --- a/app/providers/queue_service_provider.go +++ /dev/null @@ -1,28 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/queue" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/jobs" -) - -type QueueServiceProvider struct { -} - -func (receiver *QueueServiceProvider) Register(app foundation.Application) { - if err := facades.Queue().Register(receiver.Jobs()); err != nil { - panic(err.Error()) - } -} - -func (receiver *QueueServiceProvider) Boot(app foundation.Application) { - -} - -func (receiver *QueueServiceProvider) Jobs() []queue.Job { - return []queue.Job{ - &jobs.ProcessTask{}, - } -} diff --git a/app/providers/route_service_provider.go b/app/providers/route_service_provider.go deleted file mode 100644 index 3e215008..00000000 --- a/app/providers/route_service_provider.go +++ /dev/null @@ -1,48 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - contractshttp "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/http/limit" - - "github.com/TheTNB/panel/v2/app/http" - "github.com/TheTNB/panel/v2/routes" -) - -type RouteServiceProvider struct{} - -func (receiver *RouteServiceProvider) Register(app foundation.Application) { -} - -func (receiver *RouteServiceProvider) Boot(app foundation.Application) { - // Add HTTP middlewares - facades.Route().GlobalMiddleware(http.Kernel{}.Middleware()...) - - receiver.configureRateLimiting() - - routes.Plugin() - routes.Api() -} - -func (receiver *RouteServiceProvider) configureRateLimiting() { - facades.RateLimiter().ForWithLimits("login", func(ctx contractshttp.Context) []contractshttp.Limit { - return []contractshttp.Limit{ - limit.PerMinute(5).By(ctx.Request().Ip()).Response(func(ctx contractshttp.Context) { - ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{ - "message": "请求过于频繁,请等待一分钟后再试", - }) - }), - limit.PerHour(100).By(ctx.Request().Ip()).Response(func(ctx contractshttp.Context) { - ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{ - "message": "请求过于频繁,请等待一小时后再试", - }) - }), - limit.PerDay(1000).Response(func(ctx contractshttp.Context) { - ctx.Request().AbortWithStatusJson(contractshttp.StatusTooManyRequests, contractshttp.Json{ - "message": "面板遭受登录爆破攻击过多,已暂时屏蔽登录,请立刻更换面板端口", - }) - }), - } - }) -} diff --git a/app/providers/validation_service_provider.go b/app/providers/validation_service_provider.go deleted file mode 100644 index c5de9426..00000000 --- a/app/providers/validation_service_provider.go +++ /dev/null @@ -1,31 +0,0 @@ -package providers - -import ( - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/contracts/validation" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/rules" -) - -type ValidationServiceProvider struct { -} - -func (receiver *ValidationServiceProvider) Register(app foundation.Application) { - -} - -func (receiver *ValidationServiceProvider) Boot(app foundation.Application) { - if err := facades.Validation().AddRules(receiver.rules()); err != nil { - facades.Log().Infof("add rules error: %+v", err) - } -} - -func (receiver *ValidationServiceProvider) rules() []validation.Rule { - return []validation.Rule{ - &rules.Exists{}, - &rules.NotExists{}, - &rules.PathExists{}, - &rules.PathNotExists{}, - } -} diff --git a/app/rules/exists.go b/app/rules/exists.go deleted file mode 100644 index 6909eb9e..00000000 --- a/app/rules/exists.go +++ /dev/null @@ -1,55 +0,0 @@ -package rules - -import ( - "github.com/goravel/framework/contracts/validation" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" -) - -type Exists struct { -} - -// Signature The name of the rule. -func (receiver *Exists) Signature() string { - return "exists" -} - -// Passes Determine if the validation rule passes. -func (receiver *Exists) Passes(_ validation.Data, val any, options ...any) bool { - - // 第一个参数,表名称,如 categories - tableName := options[0].(string) - // 第二个参数,字段名称,如 id - fieldName := options[1].(string) - // 用户请求过来的数据 - requestValue, err := cast.ToStringE(val) - if err != nil { - return false - } - - // 判断是否为空 - if len(requestValue) == 0 { - return false - } - - // 判断是否存在 - var count int64 - query := facades.Orm().Query().Table(tableName).Where(fieldName, requestValue) - // 判断第三个参数及之后的参数是否存在 - if len(options) > 2 { - for i := 2; i < len(options); i++ { - query = query.OrWhere(options[i].(string), requestValue) - } - } - err = query.Count(&count) - if err != nil { - return false - } - - return count != 0 -} - -// Message Get the validation error message. -func (receiver *Exists) Message() string { - return "记录不存在" -} diff --git a/app/rules/not_exists.go b/app/rules/not_exists.go deleted file mode 100644 index e8231459..00000000 --- a/app/rules/not_exists.go +++ /dev/null @@ -1,55 +0,0 @@ -package rules - -import ( - "github.com/goravel/framework/contracts/validation" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" -) - -type NotExists struct { -} - -// Signature The name of the rule. -func (receiver *NotExists) Signature() string { - return "not_exists" -} - -// Passes Determine if the validation rule passes. -func (receiver *NotExists) Passes(_ validation.Data, val any, options ...any) bool { - - // 第一个参数,表名称,如 categories - tableName := options[0].(string) - // 第二个参数,字段名称,如 id - fieldName := options[1].(string) - // 用户请求过来的数据 - requestValue, err := cast.ToStringE(val) - if err != nil { - return false - } - - // 判断是否为空 - if len(requestValue) == 0 { - return false - } - - // 判断是否存在 - var count int64 - query := facades.Orm().Query().Table(tableName).Where(fieldName, requestValue) - // 判断第三个参数及之后的参数是否存在 - if len(options) > 2 { - for i := 2; i < len(options); i++ { - query = query.OrWhere(options[i].(string), requestValue) - } - } - err = query.Count(&count) - if err != nil { - return false - } - - return count == 0 -} - -// Message Get the validation error message. -func (receiver *NotExists) Message() string { - return "记录已存在" -} diff --git a/app/rules/path_exist.go b/app/rules/path_exist.go deleted file mode 100644 index 8bc0800f..00000000 --- a/app/rules/path_exist.go +++ /dev/null @@ -1,37 +0,0 @@ -package rules - -import ( - "github.com/goravel/framework/contracts/validation" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/io" -) - -type PathExists struct { -} - -// Signature The name of the rule. -func (receiver *PathExists) Signature() string { - return "path_exists" -} - -// Passes Determine if the validation rule passes. -func (receiver *PathExists) Passes(_ validation.Data, val any, options ...any) bool { - // 用户请求过来的数据 - requestValue, err := cast.ToStringE(val) - if err != nil { - return false - } - - // 判断是否为空 - if len(requestValue) == 0 { - return false - } - - return io.Exists(requestValue) -} - -// Message Get the validation error message. -func (receiver *PathExists) Message() string { - return "路径不存在" -} diff --git a/app/rules/path_not_exist.go b/app/rules/path_not_exist.go deleted file mode 100644 index c50fda88..00000000 --- a/app/rules/path_not_exist.go +++ /dev/null @@ -1,37 +0,0 @@ -package rules - -import ( - "github.com/goravel/framework/contracts/validation" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/pkg/io" -) - -type PathNotExists struct { -} - -// Signature The name of the rule. -func (receiver *PathNotExists) Signature() string { - return "path_not_exists" -} - -// Passes Determine if the validation rule passes. -func (receiver *PathNotExists) Passes(_ validation.Data, val any, options ...any) bool { - // 用户请求过来的数据 - requestValue, err := cast.ToStringE(val) - if err != nil { - return false - } - - // 判断是否为空 - if len(requestValue) == 0 { - return false - } - - return !io.Exists(requestValue) -} - -// Message Get the validation error message. -func (receiver *PathNotExists) Message() string { - return "路径已存在" -} diff --git a/bootstrap/app.go b/bootstrap/app.go deleted file mode 100644 index fdb26d3c..00000000 --- a/bootstrap/app.go +++ /dev/null @@ -1,25 +0,0 @@ -package bootstrap - -import ( - "runtime/debug" - - "github.com/gookit/validate/locales/zhcn" - "github.com/goravel/framework/foundation" - - "github.com/TheTNB/panel/v2/config" -) - -func Boot() { - debug.SetGCPercent(10) - debug.SetMemoryLimit(64 << 20) - - zhcn.RegisterGlobal() - - app := foundation.NewApplication() - - // Bootstrap the application - app.Boot() - - // Bootstrap the config. - config.Boot() -} diff --git a/cmd/README.md b/cmd/README.md new file mode 100644 index 00000000..570a8169 --- /dev/null +++ b/cmd/README.md @@ -0,0 +1,3 @@ +# cmd + +cmd 目录存放应用的入口文件。 \ No newline at end of file diff --git a/scripts/panel.sh b/cmd/app/main.go similarity index 63% rename from scripts/panel.sh rename to cmd/app/main.go index 305b7ef3..173d5af3 100644 --- a/scripts/panel.sh +++ b/cmd/app/main.go @@ -1,7 +1,4 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' +/* Copyright (C) 2022 - now HaoZi Technology Co., Ltd. This program is free software: you can redistribute it and/or modify @@ -16,6 +13,22 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -' +*/ +package main -/www/panel/panel --env="/www/panel/panel.conf" artisan panel "$@" +import "github.com/TheTNB/panel/internal/bootstrap" + +// @title 耗子面板 API +// @version 2 +// @description 耗子面板的 API 信息 + +// @contact.name 耗子科技 +// @contact.email admin@haozi.net + +// @license.name GNU Affero General Public License v3 +// @license url https://www.gnu.org/licenses/agpl-3.0.html + +// @BasePath /api +func main() { + bootstrap.Boot() +} diff --git a/scripts/frp/uninstall.sh b/cmd/cli/main.go similarity index 53% rename from scripts/frp/uninstall.sh rename to cmd/cli/main.go index e2e3db94..9ec50a63 100644 --- a/scripts/frp/uninstall.sh +++ b/cmd/cli/main.go @@ -1,7 +1,4 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' +/* Copyright (C) 2022 - now HaoZi Technology Co., Ltd. This program is free software: you can redistribute it and/or modify @@ -16,22 +13,38 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -' +*/ +package main -HR="+----------------------------------------------------" -frpPath="/www/server/frp" +import ( + "fmt" + "log" + "os" -systemctl stop frps -systemctl stop frpc -systemctl disable frps -systemctl disable frpc + "github.com/urfave/cli/v2" +) -rm -rf ${frpPath} -rm -f /etc/systemd/system/frps.service -rm -f /etc/systemd/system/frpc.service -systemctl daemon-reload +func main() { + app := &cli.App{ + Name: "panel", + HelpName: "耗子面板", + Usage: "命令行工具", + UsageText: "panel [global options] command [command options] [arguments...]", + HideVersion: true, + Commands: []*cli.Command{ + { + Name: "test", + Aliases: []string{"t"}, + Usage: "print a test message", + Action: func(c *cli.Context) error { + fmt.Println("Hello, World!") + return nil + }, + }, + }, + } -panel deletePlugin frp -echo -e $HR -echo "frp 卸载完成" -echo -e $HR + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000..20f1eba1 --- /dev/null +++ b/config/README.md @@ -0,0 +1,5 @@ +# config + +config 目录存放应用使用的各种配置文件。 + +不喜欢 toml 格式?你可以随意调整 `init.go` 以兼容你想要配置格式。 \ No newline at end of file diff --git a/config/app.go b/config/app.go deleted file mode 100644 index d7e9dd91..00000000 --- a/config/app.go +++ /dev/null @@ -1,123 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/auth" - "github.com/goravel/framework/cache" - "github.com/goravel/framework/console" - "github.com/goravel/framework/contracts/foundation" - "github.com/goravel/framework/crypt" - "github.com/goravel/framework/database" - "github.com/goravel/framework/event" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/filesystem" - "github.com/goravel/framework/hash" - "github.com/goravel/framework/http" - "github.com/goravel/framework/log" - "github.com/goravel/framework/mail" - "github.com/goravel/framework/queue" - "github.com/goravel/framework/route" - "github.com/goravel/framework/schedule" - "github.com/goravel/framework/session" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/path" - "github.com/goravel/framework/testing" - "github.com/goravel/framework/translation" - "github.com/goravel/framework/validation" - "github.com/goravel/gin" - - "github.com/TheTNB/panel/v2/app/providers" -) - -// Boot Start all init methods of the current folder to bootstrap all config. -func Boot() {} - -func init() { - config := facades.Config() - config.Add("app", map[string]any{ - // Application Name - // - // This value is the name of your application. This value is used when the - // framework needs to place the application's name in a notification or - // any other location as required by the application or its pkg. - "name": "耗子面板", - - // Application Environment - // - // This value determines the "environment" your application is currently - // running in. This may determine how you prefer to configure various - // services the application utilizes. Set this in your "panel.conf" file. - "env": config.Env("APP_ENV", "production"), - - // Application Debug Mode - "debug": config.Env("APP_DEBUG", false), - - // Application Timezone - // - // Here you may specify the default timezone for your application. - // Example: UTC, Asia/Shanghai - // More: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - "timezone": carbon.PRC, - - // Application Locale Configuration - // - // The application locale determines the default locale that will be used - // by the translation service provider.You are free to set this value - // to any of the locales which will be supported by the application. - "locale": config.Env("APP_LOCALE", "zh_CN"), - - // Application Fallback Locale - // - // The fallback locale determines the locale to use when the current one - // is not available.You may change the value to correspond to any of - // the language folders that are provided through your application. - "fallback_locale": "zh_CN", - - // Application Lang Path - // - // The path to the language files for the application. You may change - // the path to a different directory if you would like to customize it. - "lang_path": path.Executable("lang"), - - // Encryption Key - // - // 32 character string, otherwise these encrypted strings - // will not be safe. Please do this before deploying an application! - "key": config.Env("APP_KEY", ""), - - // Autoload service providers - // - // The service providers listed here will be automatically loaded on the - // request to your application. Feel free to add your own services to - // this array to grant expanded functionality to your applications. - "providers": []foundation.ServiceProvider{ - &log.ServiceProvider{}, - &console.ServiceProvider{}, - &database.ServiceProvider{}, - &cache.ServiceProvider{}, - &http.ServiceProvider{}, - &route.ServiceProvider{}, - &schedule.ServiceProvider{}, - &event.ServiceProvider{}, - &queue.ServiceProvider{}, - &mail.ServiceProvider{}, - &auth.ServiceProvider{}, - &hash.ServiceProvider{}, - &crypt.ServiceProvider{}, - &filesystem.ServiceProvider{}, - &validation.ServiceProvider{}, - &session.ServiceProvider{}, - &translation.ServiceProvider{}, - &testing.ServiceProvider{}, - &providers.AppServiceProvider{}, - &providers.AuthServiceProvider{}, - &providers.RouteServiceProvider{}, - &providers.PluginServiceProvider{}, - &providers.ConsoleServiceProvider{}, - &providers.QueueServiceProvider{}, - &providers.EventServiceProvider{}, - &providers.ValidationServiceProvider{}, - &providers.DatabaseServiceProvider{}, - &gin.ServiceProvider{}, - }, - }) -} diff --git a/config/auth.go b/config/auth.go deleted file mode 100644 index c05b7a6e..00000000 --- a/config/auth.go +++ /dev/null @@ -1,36 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("auth", map[string]any{ - // Authentication Defaults - // - // This option controls the default authentication "guard" - // reset options for your application. You may change these defaults - // as required, but they're a perfect start for most applications. - "defaults": map[string]any{ - "guard": "user", - }, - - // Authentication Guards - // - // Next, you may define every authentication guard for your application. - // Of course, a great default configuration has been defined for you - // here which uses session storage and the Eloquent user provider. - // - // All authentication drivers have a user provider. This defines how the - // users are actually retrieved out of your database or other storage - // mechanisms used by this application to persist your user's data. - // - // Supported: "jwt" - "guards": map[string]any{ - "user": map[string]any{ - "driver": "jwt", - }, - }, - }) -} diff --git a/config/cache.go b/config/cache.go deleted file mode 100644 index 286bd657..00000000 --- a/config/cache.go +++ /dev/null @@ -1,37 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("cache", map[string]any{ - // Default Cache Store - // - // This option controls the default cache connection that gets used while - // using this caching library. This connection is used when another is - // not explicitly specified when executing a given caching function. - "default": config.Env("CACHE_STORE", "memory"), - - // Cache Stores - // - // Here you may define all the cache "stores" for your application as - // well as their drivers. You may even define multiple stores for the - // same cache driver to group types of items stored in your caches. - // Available Drivers: "memory", "custom" - "stores": map[string]any{ - "memory": map[string]any{ - "driver": "memory", - }, - }, - - // Cache Key Prefix - // - // When utilizing a RAM based store such as APC or Memcached, there might - // be other applications utilizing the same cache. So, we'll specify a - // value to get prefixed to all our keys, so we can avoid collisions. - // Must: a-zA-Z0-9_- - "prefix": "panel_cache", - }) -} diff --git a/config/config.example.yml b/config/config.example.yml new file mode 100644 index 00000000..ac0ba2ab --- /dev/null +++ b/config/config.example.yml @@ -0,0 +1,12 @@ +app: + key: "a-long-string-with-32-characters" + locale: "zh_CN" + +http: + debug: true + address: ":3000" + entrance: "/" + ssl: false + +database: + debug: true diff --git a/config/cors.go b/config/cors.go deleted file mode 100644 index 21428d77..00000000 --- a/config/cors.go +++ /dev/null @@ -1,25 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("cors", map[string]any{ - // Cross-Origin Resource Sharing (CORS) Configuration - // - // Here you may configure your settings for cross-origin resource sharing - // or "CORS". This determines what cross-origin operations may execute - // in web browsers. You are free to adjust these settings as needed. - // - // To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS - "paths": []string{"1145141919810"}, // 避免框架使用CORS中间件 - "allowed_methods": []string{"*"}, - "allowed_origins": []string{"*"}, - "allowed_headers": []string{"“"}, - "exposed_headers": []string{""}, - "max_age": 0, - "supports_credentials": false, - }) -} diff --git a/config/database.go b/config/database.go deleted file mode 100644 index 86c5511d..00000000 --- a/config/database.go +++ /dev/null @@ -1,79 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" -) - -func init() { - config := facades.Config() - config.Add("database", map[string]any{ - // Default database connection name - "default": "panel", - - // Database connections - "connections": map[string]any{ - "panel": map[string]any{ - "driver": "sqlite", - "database": config.Env("DB_FILE", path.Executable("storage/panel.db")), - "prefix": "", - "singular": false, // Table name is singular - }, - }, - - // Set pool configuration - "pool": map[string]any{ - // Sets the maximum number of connections in the idle - // connection pool. - // - // If MaxOpenConns is greater than 0 but less than the new MaxIdleConns, - // then the new MaxIdleConns will be reduced to match the MaxOpenConns limit. - // - // If n <= 0, no idle connections are retained. - "max_idle_conns": 10, - // Sets the maximum number of open connections to the database. - // - // If MaxIdleConns is greater than 0 and the new MaxOpenConns is less than - // MaxIdleConns, then MaxIdleConns will be reduced to match the new - // MaxOpenConns limit. - // - // If n <= 0, then there is no limit on the number of open connections. - "max_open_conns": 100, - // Sets the maximum amount of time a connection may be idle. - // - // Expired connections may be closed lazily before reuse. - // - // If d <= 0, connections are not closed due to a connection's idle time. - // Unit: Second - "conn_max_idletime": 3600, - // Sets the maximum amount of time a connection may be reused. - // - // Expired connections may be closed lazily before reuse. - // - // If d <= 0, connections are not closed due to a connection's age. - // Unit: Second - "conn_max_lifetime": 3600, - }, - - // Migration Repository Table - // - // This table keeps track of all the migrations that have already run for - // your application. Using this information, we can determine which of - // the migrations on disk haven't actually been run in the database. - "migrations": "migrations", - - // Redis Databases - // - // Redis is an open source, fast, and advanced key-value store that also - // provides a richer body of commands than a typical key-value system - // such as APC or Memcached. - "redis": map[string]any{ - "default": map[string]any{ - "host": config.Env("REDIS_HOST", ""), - "password": config.Env("REDIS_PASSWORD", ""), - "port": config.Env("REDIS_PORT", 6379), - "database": config.Env("REDIS_DB", 0), - }, - }, - }) -} diff --git a/config/filesystems.go b/config/filesystems.go deleted file mode 100644 index 62f6b0c8..00000000 --- a/config/filesystems.go +++ /dev/null @@ -1,32 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" -) - -func init() { - config := facades.Config() - config.Add("filesystems", map[string]any{ - // Default Filesystem Disk - // - // Here you may specify the default filesystem disk that should be used - // by the framework. The "local" disk, as well as a variety of cloud - // based disks are available to your application. Just store away! - "default": "local", - - // Filesystem Disks - // - // Here you may configure as many filesystem "disks" as you wish, and you - // may even configure multiple disks of the same driver. Defaults have - // been set up for each driver as an example of the required values. - // - // Supported Drivers: "local", "custom" - "disks": map[string]any{ - "local": map[string]any{ - "driver": "local", - "root": path.Executable("storage"), - }, - }, - }) -} diff --git a/config/hashing.go b/config/hashing.go deleted file mode 100644 index c7979bf4..00000000 --- a/config/hashing.go +++ /dev/null @@ -1,40 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("hashing", map[string]any{ - // Hashing Driver - // - // This option controls the default diver that gets used - // by the framework hash facade. - // Default driver is "bcrypt". - // - // Supported Drivers: "bcrypt", "argon2id" - "driver": "argon2id", - - // Bcrypt Hashing Options - // rounds: The cost factor that should be used to compute the bcrypt hash. - // The cost factor controls how much time is needed to compute a single bcrypt hash. - // The higher the cost factor, the more hashing rounds are done. Increasing the cost - // factor by 1 doubles the necessary time. After a certain point, the returns on - // hashing time versus attacker time are diminishing, so choose your cost factor wisely. - "bcrypt": map[string]any{ - "rounds": 12, - }, - - // Argon2id Hashing Options - // memory: A memory cost, which defines the memory usage, given in kibibytes. - // time: A time cost, which defines the amount of computation - // realized and therefore the execution time, given in number of iterations. - // threads: A parallelism degree, which defines the number of parallel threads. - "argon2id": map[string]any{ - "memory": 65536, - "time": 4, - "threads": 1, - }, - }) -} diff --git a/config/http.go b/config/http.go deleted file mode 100644 index 5eca9837..00000000 --- a/config/http.go +++ /dev/null @@ -1,47 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/contracts/route" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" - ginfacades "github.com/goravel/gin/facades" -) - -func init() { - config := facades.Config() - config.Add("http", map[string]any{ - // HTTP Driver - "default": "gin", - // HTTP Drivers - "drivers": map[string]any{ - "gin": map[string]any{ - // Optional, default is 4096 KB - "body_limit": 1024 * 1024 * 4, - "header_limit": 20480, - "route": func() (route.Route, error) { - return ginfacades.Route("gin"), nil - }, - }, - }, - // HTTP URL - "url": "http://localhost", - // HTTP Host - "host": "", - // HTTP Port - "port": config.Env("APP_PORT", "8888"), - // HTTPS Configuration - "tls": map[string]any{ - // HTTPS Host - "host": "", - // HTTPS Port - "port": config.Env("APP_PORT", "8888"), - // SSL Certificate - "ssl": map[string]any{ - // ca.pem - "cert": config.Env("APP_SSL_CERT", path.Executable("storage/ssl.crt")), - // ca.key - "key": config.Env("APP_SSL_KEY", path.Executable("storage/ssl.key")), - }, - }, - }) -} diff --git a/config/jwt.go b/config/jwt.go deleted file mode 100644 index f4a1031e..00000000 --- a/config/jwt.go +++ /dev/null @@ -1,41 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("jwt", map[string]any{ - // JWT Authentication Secret - // - // Don't forget to set this in your panel.conf file, as it will be used to sign - // your tokens. A tools command is provided for this: - // `go run . artisan jwt:secret` - "secret": config.Env("JWT_SECRET", ""), - - // JWT time to live - // - // Specify the length of time (in minutes) that the token will be valid for. - // Defaults to 1 hour. - // - // You can also set this to 0, to yield a never expiring token. - // Some people may want this behaviour for e.g. a mobile app. - // This is not particularly recommended, so make sure you have appropriate - // systems in place to revoke the token if necessary. - "ttl": config.Env("JWT_TTL", 60), - - // Refresh time to live - // - // Specify the length of time (in minutes) that the token can be refreshed - // within. I.E. The user can refresh their token within a 2 week window of - // the original token being created until they must re-authenticate. - // Defaults to 2 weeks. - // - // You can also set this to 0, to yield an infinite refresh time. - // Some may want this instead of never expiring tokens for e.g. a mobile app. - // This is not particularly recommended, so make sure you have appropriate - // systems in place to revoke the token if necessary. - "refresh_ttl": config.Env("JWT_REFRESH_TTL", 20160), - }) -} diff --git a/config/logging.go b/config/logging.go deleted file mode 100644 index ed9829c1..00000000 --- a/config/logging.go +++ /dev/null @@ -1,50 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" -) - -func init() { - config := facades.Config() - config.Add("logging", map[string]any{ - // Default Log Channel - // - // This option defines the default log channel that gets used when writing - // messages to the logs. The name specified in this option should match - // one of the channels defined in the "channels" configuration array. - "default": "stack", - - // Log Channels - // - // Here you may configure the log channels for your application. - // Available Drivers: "single", "daily", "custom", "stack" - // Available Level: "debug", "info", "warning", "error", "fatal", "panic" - "channels": map[string]any{ - "stack": map[string]any{ - "driver": "stack", - "channels": []string{"daily"}, - }, - "single": map[string]any{ - "driver": "single", - "path": path.Executable("storage/logs/panel.log"), - "level": "info", - "print": true, - }, - "daily": map[string]any{ - "driver": "daily", - "path": path.Executable("storage/logs/panel.log"), - "level": "info", - "days": 7, - "print": true, - }, - "http": map[string]any{ - "driver": "daily", - "path": path.Executable("storage/logs/http.log"), - "level": "info", - "days": 7, - "print": false, - }, - }, - }) -} diff --git a/config/mail.go b/config/mail.go deleted file mode 100644 index 0439cd60..00000000 --- a/config/mail.go +++ /dev/null @@ -1,43 +0,0 @@ -package config - -import "github.com/goravel/framework/facades" - -func init() { - config := facades.Config() - config.Add("mail", map[string]any{ - // SMTP Host Address - // - // Here you may provide the host address of the SMTP server used by your - // applications. A default option is provided that is compatible with - // the Mailgun mail service which will provide reliable deliveries. - "host": config.Env("MAIL_HOST", ""), - - // SMTP Host Port - // - // This is the SMTP port used by your application to deliver e-mails to - // users of the application. Like the host we have set this value to - // stay compatible with the Mailgun e-mail application by default. - "port": config.Env("MAIL_PORT", 587), - - // -------------------------------------------------------------------------- - // Global "From" Address - // -------------------------------------------------------------------------- - // - // You may wish for all e-mails sent by your application to be sent from - // the same address. Here, you may specify a name and address that is - // used globally for all e-mails that are sent by your application. - "from": map[string]any{ - "address": config.Env("MAIL_FROM_ADDRESS", "hello@example.com"), - "name": config.Env("MAIL_FROM_NAME", "Example"), - }, - - // SMTP Server Username - // - // If your SMTP server requires a username for authentication, you should - // set it here. This will get used to authenticate with your server on - // connection. You may also set the "password" value below this one. - "username": config.Env("MAIL_USERNAME"), - - "password": config.Env("MAIL_PASSWORD"), - }) -} diff --git a/config/panel.go b/config/panel.go deleted file mode 100644 index c9dc0a47..00000000 --- a/config/panel.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("panel", map[string]any{ - "name": "耗子面板", - "version": "v2.2.27", - "ssl": config.Env("APP_SSL", false), - "entrance": config.Env("APP_ENTRANCE", "/"), - }) -} diff --git a/config/queue.go b/config/queue.go deleted file mode 100644 index 27746d34..00000000 --- a/config/queue.go +++ /dev/null @@ -1,23 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("queue", map[string]any{ - // Default Queue Connection Name - "default": "async", - - // Queue Connections - // - // Here you may configure the connection information for each server that is used by your application. - // Drivers: "sync", "async", "custom" - "connections": map[string]any{ - "async": map[string]any{ - "driver": "async", - }, - }, - }) -} diff --git a/config/session.go b/config/session.go deleted file mode 100644 index 0a722d2c..00000000 --- a/config/session.go +++ /dev/null @@ -1,85 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/path" -) - -func init() { - config := facades.Config() - config.Add("session", map[string]any{ - // Default Session Driver - // - // This option controls the default session "driver" that will be used on - // requests. By default, we will use the lightweight file session driver, but you - // may specify any of the other wonderful drivers provided here. - // - // Supported: "file" - "driver": config.Env("SESSION_DRIVER", "file"), - - // Session Lifetime - // - // Here you may specify the number of minutes that you wish the session - // to be allowed to remain idle before it expires. If you want them - // to immediately expire when the browser is closed, then you may - // indicate that via the expire_on_close configuration option. - "lifetime": config.Env("SESSION_LIFETIME", 120), - - "expire_on_close": config.Env("SESSION_EXPIRE_ON_CLOSE", false), - - // Session File Location - // - // When using the file session driver, we need a location where the - // session files may be stored. A default has been set for you, but a - // different location may be specified. This is only needed for file sessions. - "files": path.Executable("storage/framework/sessions"), - - // Session Sweeping Lottery - // - // Some session drivers must manually sweep their storage location to get - // rid of old sessions from storage. Here are the chances out of 100 that - // the sweeper will sweep the storage location. The default is 2 out of 100. - "lottery": []int{2, 100}, - - // Session Cookie Name - // - // Here you may change the name of the cookie used to identify a session - // in the application. The name specified here will get used every time - // a new session cookie is created by the framework for every driver. - "cookie": config.Env("SESSION_COOKIE", "panel_session"), - - // Session Cookie Path - // - // The session cookie path determines the path for which the cookie will - // be regarded as available.Typically, this will be the root path of - // your application, but you are free to change this when necessary. - "path": config.Env("SESSION_PATH", "/"), - - // Session Cookie Domain - // - // Here you may change the domain of the cookie used to identify a session - // in your application.This will determine which domains the cookie is - // available to in your application.A sensible default has been set. - "domain": config.Env("SESSION_DOMAIN", ""), - - // HTTPS Only Cookies - // - // By setting this option to true, session cookies will only be sent back - // to the server if the browser has an HTTPS connection. This will keep - // the cookie from being sent to you if it cannot be done securely. - "secure": config.Env("SESSION_SECURE", false), - - // HTTP Access Only - // - // Setting this to true will prevent JavaScript from accessing the value of - // the cookie, and the cookie will only be accessible through the HTTP protocol. - "http_only": config.Env("SESSION_HTTP_ONLY", true), - - // Same-Site Cookies - // - // This option determines how your cookies behave when cross-site requests - // take place, and can be used to mitigate CSRF attacks.By default, we - // will set this value to "lax" since this is a secure default value. - "same_site": config.Env("SESSION_SAME_SITE", "lax"), - }) -} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..719723f4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# docs + +docs 目录存放由 swag 命令生成的 Swagger 接口文档,你也可以放置其他文档。 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index ed3cbbe1..545233ca 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -21,99 +21,52 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/panel/cert/algorithms": { + "/info/checkUpdate": { "get": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], - "description": "获取面板证书管理支持的算法列表", "produces": [ "application/json" ], "tags": [ - "TLS证书" + "信息服务" ], - "summary": "获取算法列表", + "summary": "检查更新", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/caProviders": { + "/info/countInfo": { "get": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], - "description": "获取面板证书管理支持的 CA 提供商", "produces": [ "application/json" ], "tags": [ - "TLS证书" + "信息服务" ], - "summary": "获取 CA 提供商", + "summary": "统计信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/certs": { + "/info/homePlugins": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的证书列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取证书列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加证书到面板证书管理", "consumes": [ "application/json" ], @@ -121,82 +74,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertStore" - } - } + "信息服务" ], + "summary": "首页插件", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/certs/{id}": { + "/info/installedDbAndPhp": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的证书", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.Cert" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的证书", "consumes": [ "application/json" ], @@ -204,154 +96,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertUpdate" - } - } + "信息服务" ], + "summary": "已安装的数据库和PHP", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/deploy": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "部署面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "部署证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertDeploy" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/dns": { + "/info/nowMonitor": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 DNS 接口列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 接口列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 DNS 接口到面板证书管理", "consumes": [ "application/json" ], @@ -359,82 +118,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加 DNS 接口", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DNSStore" - } - } + "信息服务" ], + "summary": "实时监控", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/dns/{id}": { + "/info/panel": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 DNS 接口", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.CertDNS" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的 DNS 接口", "consumes": [ "application/json" ], @@ -442,105 +140,43 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DNSUpdate" - } - } + "信息服务" ], + "summary": "面板信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的 DNS 接口", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/dnsProviders": { + "/info/restart": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "信息服务" + ], + "summary": "重启面板", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.SuccessResponse" + } + } + } + } + }, + "/info/systemInfo": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理支持的 DNS 提供商", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 提供商", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/manualDNS": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取签发证书所需的 DNS 记录", "consumes": [ "application/json" ], @@ -548,53 +184,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "获取手动 DNS 记录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Obtain" - } - } + "信息服务" ], + "summary": "系统信息", "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/acme.DNSRecord" - } - } - } - } - ] + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/obtain": { + "/info/update": { "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "签发面板证书管理的证书", "consumes": [ "application/json" ], @@ -602,112 +206,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "签发证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Obtain" - } - } + "信息服务" ], + "summary": "更新面板", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/renew": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "续签面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "续签证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Renew" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/users": { + "/info/updateInfo": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 ACME 用户列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取用户列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 ACME 用户到面板证书管理", "consumes": [ "application/json" ], @@ -715,82 +228,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加 ACME 用户", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserStore" - } - } + "信息服务" ], + "summary": "版本更新信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/users/{id}": { + "/user/info/{id}": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 ACME 用户", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.CertUser" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的 ACME 用户", "consumes": [ "application/json" ], @@ -798,2461 +250,42 @@ const docTemplate = `{ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的 ACME 用户", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个容器", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ContainerCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个容器是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查容器是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个镜像是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查镜像是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个镜像的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看镜像", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有镜像列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取镜像列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的镜像", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理镜像", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/pull": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "拉取一个镜像", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "拉取镜像", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ImagePull" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个镜像", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除镜像", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/kill": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "杀死一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "杀死容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有容器列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取容器列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/logs": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的日志", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器日志", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/connect": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "连接一个容器到一个网络", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "连接容器到网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkConnectDisConnect" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个网络", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/disconnect": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "从一个网络断开一个容器", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "从网络断开容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkConnectDisConnect" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个网络是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查网络是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个网络的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看网络", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有网络列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取网络列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的网络", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理网络", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个网络", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理容器", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/rename": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "重命名一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "重命名容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ContainerRename" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/restart": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "重启一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "重启容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/search": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "根据容器名称搜索容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "搜索容器", - "parameters": [ - { - "type": "string", - "description": "容器名称", - "name": "name", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/start": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "启动一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "启动容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/stats": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的状态信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器状态", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/stop": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "停止一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "停止容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/unpause": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "取消暂停一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "取消暂停容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个卷", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建卷", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.VolumeCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个卷是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查卷是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个卷的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看卷", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有卷列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取卷列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的卷", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理卷", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个卷", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除卷", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/archive": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "压缩文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "压缩文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Archive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/content": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取给定路径的文件内容", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件内容", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/copy": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "复制文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "复制文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Copy" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "创建文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NotExist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/delete": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除给定路径的文件/目录", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "删除文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Exist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/download": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "下载给定路径的文件", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "下载文件", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/info": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取给定路径的文件/目录信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件/目录信息", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/list": { - "get": { - "description": "获取给定路径的文件/目录列表", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件/目录列表", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - }, - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/move": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "移动文件/目录到给定路径,等效于重命名", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "移动文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Move" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/permission": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "修改给定路径的文件/目录权限", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "修改文件/目录权限", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Permission" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/remoteDownload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "下载远程文件到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "下载远程文件", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NotExist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/save": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "保存给定路径的文件内容", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "保存文件内容", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Save" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/search": { - "post": { - "description": "通过关键词搜索给定路径的文件/目录", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "搜索文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Search" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/unArchive": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "解压文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "解压文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UnArchive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/upload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "上传文件到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "上传文件", - "parameters": [ - { - "type": "file", - "description": "file", - "name": "file", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "path", - "name": "path", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/install": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "安装插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/isInstalled": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "检查插件是否已安装", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "插件列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/uninstall": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "卸载插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/update": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "更新插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/updateShow": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "更新插件首页显示状态", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - }, - { - "type": "boolean", - "description": "request", - "name": "show", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/https": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "获取面板 HTTPS 设置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "更新面板 HTTPS 设置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Https" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "设置列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/update": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "更新设置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_setting.Update" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/disable": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "禁用服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/enable": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "启用服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/isEnabled": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "是否启用服务", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "data", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/reload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "重载服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/restart": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "重启服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/start": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "启动服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/status": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "服务状态", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "data", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/stop": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "停止服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/user/info": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "用户鉴权" + "用户服务" ], "summary": "用户信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/user/login": { + "/user/isLogin": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户服务" + ], + "summary": "是否登录", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.SuccessResponse" + } + } + } + } + }, + "/user/login": { "post": { "consumes": [ "application/json" @@ -3261,7 +294,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "用户鉴权" + "用户服务" ], "summary": "登录", "parameters": [ @@ -3271,7 +304,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.Login" + "$ref": "#/definitions/request.UserLogin" } } ], @@ -3279,1725 +312,56 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - }, - "403": { - "description": "用户名或密码错误", - "schema": { - "$ref": "#/definitions/controllers.ErrorResponse" - } - }, - "500": { - "description": "系统内部错误", - "schema": { - "$ref": "#/definitions/controllers.ErrorResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/user/logout": { + "/user/logout": { "post": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], "produces": [ "application/json" ], "tags": [ - "用户鉴权" + "用户服务" ], "summary": "登出", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } - }, - "/panel/website/backupList": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站备份列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/types.BackupFile" - } - } - } - } - ] - } - } - } - } - }, - "/panel/website/defaultConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取默认配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - ] - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "保存默认配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/website/deleteBackup": { - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "删除网站备份", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DeleteBackup" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/website/uploadBackup": { - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "上传网站备份", - "parameters": [ - { - "type": "file", - "description": "备份文件", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "添加网站", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Add" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/delete": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "删除网站", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Delete" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/types.WebsiteAdd" - } - } - } - ] - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "保存网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.SaveConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/createBackup": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "创建网站备份", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/log": { - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "清空网站日志", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/resetConfig": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "重置网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/restoreBackup": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "还原网站备份", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/status": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站状态", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/updateRemark": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "更新网站备注", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/frp/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Frp 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Frp" - ], - "summary": "获取配置", - "parameters": [ - { - "type": "string", - "description": "服务", - "name": "service", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Frp 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Frp" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/gitea/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Gitea 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Gitea" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Gitea 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Gitea" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/clearErrorLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "清空错误日志", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "保存配置", - "parameters": [ - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/errorLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取错误日志", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/load": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取负载状态", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/clearErrorLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "清空错误日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/clearSlowLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "清空慢日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "保存配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/errorLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取错误日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/extensions": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取扩展列表", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "安装扩展", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "slug", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "卸载扩展", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "slug", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/fpmConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取 FPM 配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "保存 FPM 配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/load": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取负载状态", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/slowLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取慢日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/podman/registryConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Podman 注册表配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "获取注册表配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Podman 注册表配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "更新注册表配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UpdateRegistryConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/podman/storageConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Podman 存储配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "获取存储配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Podman 存储配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "更新存储配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UpdateStorageConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Rsync 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Rsync 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/modules": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "列出所有 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "列出模块", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "添加模块", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Create" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/modules/{name}": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "更新模块", - "parameters": [ - { - "type": "string", - "description": "模块名称", - "name": "name", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "删除模块", - "parameters": [ - { - "type": "string", - "description": "模块名称", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/swagger": { - "get": { - "description": "Swagger UI", - "tags": [ - "Swagger" - ], - "summary": "Swagger UI", - "responses": { - "200": { - "description": "OK" - }, - "500": { - "description": "Internal Server Error" - } - } - } } }, "definitions": { - "acme.DNSParam": { + "request.UserLogin": { "type": "object", + "required": [ + "password", + "username" + ], "properties": { - "access_key": { - "type": "string" + "password": { + "type": "string", + "maxLength": 255, + "minLength": 6 }, - "api_key": { - "type": "string" - }, - "id": { - "type": "string" - }, - "secret_key": { - "type": "string" - }, - "token": { - "type": "string" + "username": { + "type": "string", + "maxLength": 255, + "minLength": 3 } } }, - "acme.DNSRecord": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "controllers.ErrorResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "controllers.SuccessResponse": { + "service.SuccessResponse": { "type": "object", "properties": { "data": {}, @@ -5005,1009 +369,6 @@ const docTemplate = `{ "type": "string" } } - }, - "github_com_TheTNB_panel_app_http_requests_container.ID": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - }, - "service": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update": { - "type": "object", - "properties": { - "auth_user": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "hosts_allow": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_setting.Update": { - "type": "object", - "properties": { - "backup_path": { - "type": "string" - }, - "email": { - "type": "string" - }, - "entrance": { - "type": "string" - }, - "language": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "website_path": { - "type": "string" - } - } - }, - "github_com_goravel_framework_support_carbon.DateTime": { - "type": "object", - "properties": { - "error": {} - } - }, - "models.Cert": { - "type": "object", - "properties": { - "auto_renew": { - "description": "自动续签", - "type": "boolean" - }, - "cert": { - "description": "证书内容", - "type": "string" - }, - "cert_url": { - "description": "证书 URL (续签时使用)", - "type": "string" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "dns": { - "$ref": "#/definitions/models.CertDNS" - }, - "dns_id": { - "description": "关联的 DNS ID", - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "integer" - }, - "key": { - "description": "私钥内容", - "type": "string" - }, - "type": { - "description": "证书类型 (P256, P384, 2048, 4096)", - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "user": { - "$ref": "#/definitions/models.CertUser" - }, - "user_id": { - "description": "关联的 ACME 用户 ID", - "type": "integer" - }, - "website": { - "$ref": "#/definitions/models.Website" - }, - "website_id": { - "description": "关联的网站 ID", - "type": "integer" - } - } - }, - "models.CertDNS": { - "type": "object", - "properties": { - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "dns_param": { - "$ref": "#/definitions/acme.DNSParam" - }, - "id": { - "type": "integer" - }, - "name": { - "description": "备注名称", - "type": "string" - }, - "type": { - "description": "DNS 提供商 (dnspod, tencent, aliyun, cloudflare)", - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "models.CertUser": { - "type": "object", - "properties": { - "ca": { - "description": "CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass)", - "type": "string" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - }, - "private_key": { - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "models.Website": { - "type": "object", - "properties": { - "cert": { - "$ref": "#/definitions/models.Cert" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "integer" - }, - "remark": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "status": { - "type": "boolean" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "requests.Add": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "db_name": { - "type": "string" - }, - "db_password": { - "type": "string" - }, - "db_type": { - "type": "string" - }, - "db_user": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "requests.Archive": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "paths": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "requests.CertDeploy": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.CertStore": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "dns_id": { - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string" - }, - "user_id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.CertUpdate": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "dns_id": { - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "user_id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.ContainerCreate": { - "type": "object", - "properties": { - "auto_remove": { - "type": "boolean" - }, - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "cpu_shares": { - "type": "integer" - }, - "cpus": { - "type": "integer" - }, - "entrypoint": { - "type": "array", - "items": { - "type": "string" - } - }, - "env": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "image": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "memory": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "network": { - "type": "string" - }, - "open_stdin": { - "type": "boolean" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/types.ContainerPort" - } - }, - "privileged": { - "type": "boolean" - }, - "publish_all_ports": { - "type": "boolean" - }, - "restart_policy": { - "type": "string" - }, - "tty": { - "type": "boolean" - }, - "volumes": { - "type": "array", - "items": { - "$ref": "#/definitions/types.ContainerVolume" - } - } - } - }, - "requests.ContainerRename": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "requests.Copy": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - } - }, - "requests.Create": { - "type": "object", - "properties": { - "auth_user": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "hosts_allow": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "requests.DNSStore": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/acme.DNSParam" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "requests.DNSUpdate": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/acme.DNSParam" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "requests.Delete": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "path": { - "type": "boolean" - } - } - }, - "requests.DeleteBackup": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - }, - "requests.Exist": { - "type": "object", - "properties": { - "path": { - "type": "string" - } - } - }, - "requests.Https": { - "type": "object", - "properties": { - "cert": { - "type": "string" - }, - "https": { - "type": "boolean" - }, - "key": { - "type": "string" - } - } - }, - "requests.ImagePull": { - "type": "object", - "properties": { - "auth": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "requests.Login": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "requests.Move": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - } - }, - "requests.NetworkConnectDisConnect": { - "type": "object", - "properties": { - "container": { - "type": "string" - }, - "network": { - "type": "string" - } - } - }, - "requests.NetworkCreate": { - "type": "object", - "properties": { - "driver": { - "type": "string" - }, - "ipv4": { - "$ref": "#/definitions/types.ContainerNetwork" - }, - "ipv6": { - "$ref": "#/definitions/types.ContainerNetwork" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "name": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - } - } - }, - "requests.NotExist": { - "type": "object", - "properties": { - "path": { - "type": "string" - } - } - }, - "requests.Obtain": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "requests.Permission": { - "type": "object", - "properties": { - "group": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.Renew": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "requests.Save": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.SaveConfig": { - "type": "object", - "properties": { - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "hsts": { - "type": "boolean" - }, - "http_redirect": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "index": { - "type": "string" - }, - "open_basedir": { - "type": "boolean" - }, - "path": { - "type": "string" - }, - "php": { - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "raw": { - "type": "string" - }, - "rewrite": { - "type": "string" - }, - "root": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "ssl_certificate": { - "type": "string" - }, - "ssl_certificate_key": { - "type": "string" - }, - "tls_ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "waf": { - "type": "boolean" - }, - "waf_cache": { - "type": "string" - }, - "waf_cc_deny": { - "type": "string" - }, - "waf_mode": { - "type": "string" - } - } - }, - "requests.Search": { - "type": "object", - "properties": { - "keyword": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.UnArchive": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.UpdateRegistryConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "requests.UpdateStorageConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "requests.UserStore": { - "type": "object", - "properties": { - "ca": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - } - } - }, - "requests.UserUpdate": { - "type": "object", - "properties": { - "ca": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - } - } - }, - "requests.VolumeCreate": { - "type": "object", - "properties": { - "driver": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "name": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - } - } - }, - "types.BackupFile": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "size": { - "type": "string" - } - } - }, - "types.ContainerNetwork": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "gateway": { - "type": "string" - }, - "ip_range": { - "type": "string" - }, - "subnet": { - "type": "string" - } - } - }, - "types.ContainerPort": { - "type": "object", - "properties": { - "container_end": { - "type": "integer" - }, - "container_start": { - "type": "integer" - }, - "host": { - "type": "string" - }, - "host_end": { - "type": "integer" - }, - "host_start": { - "type": "integer" - }, - "protocol": { - "type": "string" - } - } - }, - "types.ContainerVolume": { - "type": "object", - "properties": { - "container": { - "type": "string" - }, - "host": { - "type": "string" - }, - "mode": { - "type": "string" - } - } - }, - "types.KV": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "types.WebsiteAdd": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "db_name": { - "type": "string" - }, - "db_password": { - "type": "string" - }, - "db_type": { - "type": "string" - }, - "db_user": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "remark": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "status": { - "type": "boolean" - } - } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index bc9f45d9..5dbe23f4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -14,99 +14,52 @@ }, "basePath": "/api", "paths": { - "/panel/cert/algorithms": { + "/info/checkUpdate": { "get": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], - "description": "获取面板证书管理支持的算法列表", "produces": [ "application/json" ], "tags": [ - "TLS证书" + "信息服务" ], - "summary": "获取算法列表", + "summary": "检查更新", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/caProviders": { + "/info/countInfo": { "get": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], - "description": "获取面板证书管理支持的 CA 提供商", "produces": [ "application/json" ], "tags": [ - "TLS证书" + "信息服务" ], - "summary": "获取 CA 提供商", + "summary": "统计信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/certs": { + "/info/homePlugins": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的证书列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取证书列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加证书到面板证书管理", "consumes": [ "application/json" ], @@ -114,82 +67,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertStore" - } - } + "信息服务" ], + "summary": "首页插件", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/certs/{id}": { + "/info/installedDbAndPhp": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的证书", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.Cert" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的证书", "consumes": [ "application/json" ], @@ -197,154 +89,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertUpdate" - } - } + "信息服务" ], + "summary": "已安装的数据库和PHP", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除证书", - "parameters": [ - { - "type": "integer", - "description": "证书 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/deploy": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "部署面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "部署证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.CertDeploy" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/dns": { + "/info/nowMonitor": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 DNS 接口列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 接口列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 DNS 接口到面板证书管理", "consumes": [ "application/json" ], @@ -352,82 +111,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加 DNS 接口", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DNSStore" - } - } + "信息服务" ], + "summary": "实时监控", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/dns/{id}": { + "/info/panel": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 DNS 接口", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.CertDNS" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的 DNS 接口", "consumes": [ "application/json" ], @@ -435,105 +133,43 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DNSUpdate" - } - } + "信息服务" ], + "summary": "面板信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的 DNS 接口", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除 DNS 接口", - "parameters": [ - { - "type": "integer", - "description": "DNS 接口 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/dnsProviders": { + "/info/restart": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "信息服务" + ], + "summary": "重启面板", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.SuccessResponse" + } + } + } + } + }, + "/info/systemInfo": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理支持的 DNS 提供商", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 DNS 提供商", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/manualDNS": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取签发证书所需的 DNS 记录", "consumes": [ "application/json" ], @@ -541,53 +177,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "获取手动 DNS 记录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Obtain" - } - } + "信息服务" ], + "summary": "系统信息", "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/acme.DNSRecord" - } - } - } - } - ] + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/obtain": { + "/info/update": { "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "签发面板证书管理的证书", "consumes": [ "application/json" ], @@ -595,112 +199,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "签发证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Obtain" - } - } + "信息服务" ], + "summary": "更新面板", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/renew": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "续签面板证书管理的证书", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "续签证书", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Renew" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/cert/users": { + "/info/updateInfo": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 ACME 用户列表", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取用户列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 ACME 用户到面板证书管理", "consumes": [ "application/json" ], @@ -708,82 +221,21 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "添加 ACME 用户", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserStore" - } - } + "信息服务" ], + "summary": "版本更新信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/cert/users/{id}": { + "/user/info/{id}": { "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取面板证书管理的 ACME 用户", - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "获取 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.CertUser" - } - } - } - ] - } - } - } - }, - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新面板证书管理的 ACME 用户", "consumes": [ "application/json" ], @@ -791,2461 +243,42 @@ "application/json" ], "tags": [ - "TLS证书" - ], - "summary": "更新 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UserUpdate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除面板证书管理的 ACME 用户", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "TLS证书" - ], - "summary": "删除 ACME 用户", - "parameters": [ - { - "type": "integer", - "description": "用户 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个容器", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ContainerCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个容器是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查容器是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个镜像是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查镜像是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个镜像的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看镜像", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有镜像列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取镜像列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的镜像", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理镜像", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/pull": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "拉取一个镜像", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "拉取镜像", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ImagePull" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/image/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个镜像", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除镜像", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/kill": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "杀死一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "杀死容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有容器列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取容器列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/logs": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的日志", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器日志", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/connect": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "连接一个容器到一个网络", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "连接容器到网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkConnectDisConnect" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个网络", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/disconnect": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "从一个网络断开一个容器", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "从网络断开容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NetworkConnectDisConnect" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个网络是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查网络是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个网络的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看网络", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有网络列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取网络列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的网络", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理网络", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/network/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个网络", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除网络", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理容器", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/rename": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "重命名一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "重命名容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.ContainerRename" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/restart": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "重启一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "重启容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/search": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "根据容器名称搜索容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "搜索容器", - "parameters": [ - { - "type": "string", - "description": "容器名称", - "name": "name", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/start": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "启动一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "启动容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/stats": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个容器的状态信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看容器状态", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/stop": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "停止一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "停止容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/unpause": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "取消暂停一个容器", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "取消暂停容器", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建一个卷", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "创建卷", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.VolumeCreate" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/exist": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "检查一个卷是否存在", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "检查卷是否存在", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/inspect": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "查看一个卷的详细信息", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "查看卷", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取所有卷列表", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "获取卷列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/prune": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "清理无用的卷", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "清理卷", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/container/volume/remove": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除一个卷", - "produces": [ - "application/json" - ], - "tags": [ - "容器" - ], - "summary": "删除卷", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/archive": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "压缩文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "压缩文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Archive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/content": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取给定路径的文件内容", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件内容", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/copy": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "复制文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "复制文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Copy" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/create": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "创建文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "创建文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NotExist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/delete": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除给定路径的文件/目录", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "删除文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Exist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/download": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "下载给定路径的文件", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "下载文件", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/info": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取给定路径的文件/目录信息", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件/目录信息", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/list": { - "get": { - "description": "获取给定路径的文件/目录列表", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "获取文件/目录列表", - "parameters": [ - { - "type": "string", - "name": "path", - "in": "query" - }, - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/move": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "移动文件/目录到给定路径,等效于重命名", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "移动文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Move" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/permission": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "修改给定路径的文件/目录权限", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "修改文件/目录权限", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Permission" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/remoteDownload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "下载远程文件到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "下载远程文件", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.NotExist" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/save": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "保存给定路径的文件内容", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "保存文件内容", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Save" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/search": { - "post": { - "description": "通过关键词搜索给定路径的文件/目录", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "搜索文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Search" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/unArchive": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "解压文件/目录到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "解压文件/目录", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UnArchive" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/file/upload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "上传文件到给定路径", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "文件" - ], - "summary": "上传文件", - "parameters": [ - { - "type": "file", - "description": "file", - "name": "file", - "in": "formData", - "required": true - }, - { - "type": "string", - "description": "path", - "name": "path", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/install": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "安装插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/isInstalled": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "检查插件是否已安装", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "插件列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/uninstall": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "卸载插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/update": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "更新插件", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/plugin/updateShow": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件" - ], - "summary": "更新插件首页显示状态", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "slug", - "in": "query", - "required": true - }, - { - "type": "boolean", - "description": "request", - "name": "show", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/https": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "获取面板 HTTPS 设置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "更新面板 HTTPS 设置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Https" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/list": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "设置列表", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/setting/update": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "面板设置" - ], - "summary": "更新设置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_setting.Update" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/disable": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "禁用服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/enable": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "启用服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/isEnabled": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "是否启用服务", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "data", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/reload": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "重载服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/restart": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "重启服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/start": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "启动服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/status": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "服务状态", - "parameters": [ - { - "type": "string", - "description": "request", - "name": "data", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/system/service/stop": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "系统" - ], - "summary": "停止服务", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/user/info": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "用户鉴权" + "用户服务" ], "summary": "用户信息", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/user/login": { + "/user/isLogin": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户服务" + ], + "summary": "是否登录", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/service.SuccessResponse" + } + } + } + } + }, + "/user/login": { "post": { "consumes": [ "application/json" @@ -3254,7 +287,7 @@ "application/json" ], "tags": [ - "用户鉴权" + "用户服务" ], "summary": "登录", "parameters": [ @@ -3264,7 +297,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/requests.Login" + "$ref": "#/definitions/request.UserLogin" } } ], @@ -3272,1725 +305,56 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - }, - "403": { - "description": "用户名或密码错误", - "schema": { - "$ref": "#/definitions/controllers.ErrorResponse" - } - }, - "500": { - "description": "系统内部错误", - "schema": { - "$ref": "#/definitions/controllers.ErrorResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } }, - "/panel/user/logout": { + "/user/logout": { "post": { - "security": [ - { - "BearerToken": [] - } + "consumes": [ + "application/json" ], "produces": [ "application/json" ], "tags": [ - "用户鉴权" + "用户服务" ], "summary": "登出", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" + "$ref": "#/definitions/service.SuccessResponse" } } } } - }, - "/panel/website/backupList": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站备份列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/types.BackupFile" - } - } - } - } - ] - } - } - } - } - }, - "/panel/website/defaultConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取默认配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - ] - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "保存默认配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/website/deleteBackup": { - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "删除网站备份", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.DeleteBackup" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/website/uploadBackup": { - "put": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "上传网站备份", - "parameters": [ - { - "type": "file", - "description": "备份文件", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站列表", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "添加网站", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Add" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/delete": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "删除网站", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Delete" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/controllers.SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/types.WebsiteAdd" - } - } - } - ] - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "保存网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.SaveConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/createBackup": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "创建网站备份", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/log": { - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "清空网站日志", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/resetConfig": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "重置网站配置", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/restoreBackup": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "还原网站备份", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/status": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "获取网站状态", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/panel/websites/{id}/updateRemark": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "网站" - ], - "summary": "更新网站备注", - "parameters": [ - { - "type": "integer", - "description": "网站 ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/frp/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Frp 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Frp" - ], - "summary": "获取配置", - "parameters": [ - { - "type": "string", - "description": "服务", - "name": "service", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Frp 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Frp" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/gitea/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Gitea 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Gitea" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Gitea 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Gitea" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/clearErrorLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "清空错误日志", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "保存配置", - "parameters": [ - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/errorLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取错误日志", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/openresty/load": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-OpenResty" - ], - "summary": "获取负载状态", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/clearErrorLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "清空错误日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/clearSlowLog": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "清空慢日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "保存配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/errorLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取错误日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/extensions": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取扩展列表", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "安装扩展", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "slug", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "卸载扩展", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "slug", - "name": "slug", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/fpmConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取 FPM 配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "保存 FPM 配置", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - }, - { - "description": "配置", - "name": "config", - "in": "body", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/load": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取负载状态", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/php/{version}/slowLog": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "插件-PHP" - ], - "summary": "获取慢日志", - "parameters": [ - { - "type": "integer", - "description": "PHP 版本", - "name": "version", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/podman/registryConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Podman 注册表配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "获取注册表配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Podman 注册表配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "更新注册表配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UpdateRegistryConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/podman/storageConfig": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Podman 存储配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "获取存储配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Podman 存储配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Podman" - ], - "summary": "更新存储配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.UpdateStorageConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/config": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "获取 Rsync 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "获取配置", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Rsync 配置", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "更新配置", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/modules": { - "get": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "列出所有 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "列出模块", - "parameters": [ - { - "type": "integer", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "添加 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "添加模块", - "parameters": [ - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/requests.Create" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/plugins/rsync/modules/{name}": { - "post": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "更新 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "更新模块", - "parameters": [ - { - "type": "string", - "description": "模块名称", - "name": "name", - "in": "path", - "required": true - }, - { - "description": "request", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerToken": [] - } - ], - "description": "删除 Rsync 模块", - "produces": [ - "application/json" - ], - "tags": [ - "插件-Rsync" - ], - "summary": "删除模块", - "parameters": [ - { - "type": "string", - "description": "模块名称", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/controllers.SuccessResponse" - } - } - } - } - }, - "/swagger": { - "get": { - "description": "Swagger UI", - "tags": [ - "Swagger" - ], - "summary": "Swagger UI", - "responses": { - "200": { - "description": "OK" - }, - "500": { - "description": "Internal Server Error" - } - } - } } }, "definitions": { - "acme.DNSParam": { + "request.UserLogin": { "type": "object", + "required": [ + "password", + "username" + ], "properties": { - "access_key": { - "type": "string" + "password": { + "type": "string", + "maxLength": 255, + "minLength": 6 }, - "api_key": { - "type": "string" - }, - "id": { - "type": "string" - }, - "secret_key": { - "type": "string" - }, - "token": { - "type": "string" + "username": { + "type": "string", + "maxLength": 255, + "minLength": 3 } } }, - "acme.DNSRecord": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "controllers.ErrorResponse": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "controllers.SuccessResponse": { + "service.SuccessResponse": { "type": "object", "properties": { "data": {}, @@ -4998,1009 +362,6 @@ "type": "string" } } - }, - "github_com_TheTNB_panel_app_http_requests_container.ID": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - }, - "service": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update": { - "type": "object", - "properties": { - "auth_user": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "hosts_allow": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "github_com_TheTNB_panel_app_http_requests_setting.Update": { - "type": "object", - "properties": { - "backup_path": { - "type": "string" - }, - "email": { - "type": "string" - }, - "entrance": { - "type": "string" - }, - "language": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "website_path": { - "type": "string" - } - } - }, - "github_com_goravel_framework_support_carbon.DateTime": { - "type": "object", - "properties": { - "error": {} - } - }, - "models.Cert": { - "type": "object", - "properties": { - "auto_renew": { - "description": "自动续签", - "type": "boolean" - }, - "cert": { - "description": "证书内容", - "type": "string" - }, - "cert_url": { - "description": "证书 URL (续签时使用)", - "type": "string" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "dns": { - "$ref": "#/definitions/models.CertDNS" - }, - "dns_id": { - "description": "关联的 DNS ID", - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "integer" - }, - "key": { - "description": "私钥内容", - "type": "string" - }, - "type": { - "description": "证书类型 (P256, P384, 2048, 4096)", - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "user": { - "$ref": "#/definitions/models.CertUser" - }, - "user_id": { - "description": "关联的 ACME 用户 ID", - "type": "integer" - }, - "website": { - "$ref": "#/definitions/models.Website" - }, - "website_id": { - "description": "关联的网站 ID", - "type": "integer" - } - } - }, - "models.CertDNS": { - "type": "object", - "properties": { - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "dns_param": { - "$ref": "#/definitions/acme.DNSParam" - }, - "id": { - "type": "integer" - }, - "name": { - "description": "备注名称", - "type": "string" - }, - "type": { - "description": "DNS 提供商 (dnspod, tencent, aliyun, cloudflare)", - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "models.CertUser": { - "type": "object", - "properties": { - "ca": { - "description": "CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass)", - "type": "string" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - }, - "private_key": { - "type": "string" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "models.Website": { - "type": "object", - "properties": { - "cert": { - "$ref": "#/definitions/models.Cert" - }, - "created_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "integer" - }, - "remark": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "status": { - "type": "boolean" - }, - "updated_at": { - "$ref": "#/definitions/github_com_goravel_framework_support_carbon.DateTime" - } - } - }, - "requests.Add": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "db_name": { - "type": "string" - }, - "db_password": { - "type": "string" - }, - "db_type": { - "type": "string" - }, - "db_user": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - } - } - }, - "requests.Archive": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "paths": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "requests.CertDeploy": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.CertStore": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "dns_id": { - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "type": { - "type": "string" - }, - "user_id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.CertUpdate": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "dns_id": { - "type": "integer" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "integer" - }, - "type": { - "type": "string" - }, - "user_id": { - "type": "integer" - }, - "website_id": { - "type": "integer" - } - } - }, - "requests.ContainerCreate": { - "type": "object", - "properties": { - "auto_remove": { - "type": "boolean" - }, - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "cpu_shares": { - "type": "integer" - }, - "cpus": { - "type": "integer" - }, - "entrypoint": { - "type": "array", - "items": { - "type": "string" - } - }, - "env": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "image": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "memory": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "network": { - "type": "string" - }, - "open_stdin": { - "type": "boolean" - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/types.ContainerPort" - } - }, - "privileged": { - "type": "boolean" - }, - "publish_all_ports": { - "type": "boolean" - }, - "restart_policy": { - "type": "string" - }, - "tty": { - "type": "boolean" - }, - "volumes": { - "type": "array", - "items": { - "$ref": "#/definitions/types.ContainerVolume" - } - } - } - }, - "requests.ContainerRename": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "requests.Copy": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - } - }, - "requests.Create": { - "type": "object", - "properties": { - "auth_user": { - "type": "string" - }, - "comment": { - "type": "string" - }, - "hosts_allow": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, - "requests.DNSStore": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/acme.DNSParam" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "requests.DNSUpdate": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/acme.DNSParam" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "requests.Delete": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "path": { - "type": "boolean" - } - } - }, - "requests.DeleteBackup": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - }, - "requests.Exist": { - "type": "object", - "properties": { - "path": { - "type": "string" - } - } - }, - "requests.Https": { - "type": "object", - "properties": { - "cert": { - "type": "string" - }, - "https": { - "type": "boolean" - }, - "key": { - "type": "string" - } - } - }, - "requests.ImagePull": { - "type": "object", - "properties": { - "auth": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "requests.Login": { - "type": "object", - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "requests.Move": { - "type": "object", - "properties": { - "source": { - "type": "string" - }, - "target": { - "type": "string" - } - } - }, - "requests.NetworkConnectDisConnect": { - "type": "object", - "properties": { - "container": { - "type": "string" - }, - "network": { - "type": "string" - } - } - }, - "requests.NetworkCreate": { - "type": "object", - "properties": { - "driver": { - "type": "string" - }, - "ipv4": { - "$ref": "#/definitions/types.ContainerNetwork" - }, - "ipv6": { - "$ref": "#/definitions/types.ContainerNetwork" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "name": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - } - } - }, - "requests.NotExist": { - "type": "object", - "properties": { - "path": { - "type": "string" - } - } - }, - "requests.Obtain": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "requests.Permission": { - "type": "object", - "properties": { - "group": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.Renew": { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - "requests.Save": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.SaveConfig": { - "type": "object", - "properties": { - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "hsts": { - "type": "boolean" - }, - "http_redirect": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "index": { - "type": "string" - }, - "open_basedir": { - "type": "boolean" - }, - "path": { - "type": "string" - }, - "php": { - "type": "integer" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "raw": { - "type": "string" - }, - "rewrite": { - "type": "string" - }, - "root": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "ssl_certificate": { - "type": "string" - }, - "ssl_certificate_key": { - "type": "string" - }, - "tls_ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "waf": { - "type": "boolean" - }, - "waf_cache": { - "type": "string" - }, - "waf_cc_deny": { - "type": "string" - }, - "waf_mode": { - "type": "string" - } - } - }, - "requests.Search": { - "type": "object", - "properties": { - "keyword": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.UnArchive": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "requests.UpdateRegistryConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "requests.UpdateStorageConfig": { - "type": "object", - "properties": { - "config": { - "type": "string" - } - } - }, - "requests.UserStore": { - "type": "object", - "properties": { - "ca": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - } - } - }, - "requests.UserUpdate": { - "type": "object", - "properties": { - "ca": { - "type": "string" - }, - "email": { - "type": "string" - }, - "hmac_encoded": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "key_type": { - "type": "string" - }, - "kid": { - "type": "string" - } - } - }, - "requests.VolumeCreate": { - "type": "object", - "properties": { - "driver": { - "type": "string" - }, - "labels": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - }, - "name": { - "type": "string" - }, - "options": { - "type": "array", - "items": { - "$ref": "#/definitions/types.KV" - } - } - } - }, - "types.BackupFile": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "size": { - "type": "string" - } - } - }, - "types.ContainerNetwork": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "gateway": { - "type": "string" - }, - "ip_range": { - "type": "string" - }, - "subnet": { - "type": "string" - } - } - }, - "types.ContainerPort": { - "type": "object", - "properties": { - "container_end": { - "type": "integer" - }, - "container_start": { - "type": "integer" - }, - "host": { - "type": "string" - }, - "host_end": { - "type": "integer" - }, - "host_start": { - "type": "integer" - }, - "protocol": { - "type": "string" - } - } - }, - "types.ContainerVolume": { - "type": "object", - "properties": { - "container": { - "type": "string" - }, - "host": { - "type": "string" - }, - "mode": { - "type": "string" - } - } - }, - "types.KV": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "types.WebsiteAdd": { - "type": "object", - "properties": { - "db": { - "type": "boolean" - }, - "db_name": { - "type": "string" - }, - "db_password": { - "type": "string" - }, - "db_type": { - "type": "string" - }, - "db_user": { - "type": "string" - }, - "domains": { - "type": "array", - "items": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "php": { - "type": "string" - }, - "ports": { - "type": "array", - "items": { - "type": "integer" - } - }, - "remark": { - "type": "string" - }, - "ssl": { - "type": "boolean" - }, - "status": { - "type": "boolean" - } - } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4cd2f3e9..56d9c8be 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,692 +1,25 @@ basePath: /api definitions: - acme.DNSParam: + request.UserLogin: properties: - access_key: + password: + maxLength: 255 + minLength: 6 type: string - api_key: - type: string - id: - type: string - secret_key: - type: string - token: + username: + maxLength: 255 + minLength: 3 type: string + required: + - password + - username type: object - acme.DNSRecord: - properties: - key: - type: string - value: - type: string - type: object - controllers.ErrorResponse: - properties: - message: - type: string - type: object - controllers.SuccessResponse: + service.SuccessResponse: properties: data: {} message: type: string type: object - github_com_TheTNB_panel_app_http_requests_container.ID: - properties: - id: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig: - properties: - config: - type: string - service: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig: - properties: - config: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update: - properties: - auth_user: - type: string - comment: - type: string - hosts_allow: - type: string - name: - type: string - path: - type: string - secret: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig: - properties: - config: - type: string - type: object - github_com_TheTNB_panel_app_http_requests_setting.Update: - properties: - backup_path: - type: string - email: - type: string - entrance: - type: string - language: - type: string - name: - type: string - password: - type: string - port: - type: integer - username: - type: string - website_path: - type: string - type: object - github_com_goravel_framework_support_carbon.DateTime: - properties: - error: {} - type: object - models.Cert: - properties: - auto_renew: - description: 自动续签 - type: boolean - cert: - description: 证书内容 - type: string - cert_url: - description: 证书 URL (续签时使用) - type: string - created_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - dns: - $ref: '#/definitions/models.CertDNS' - dns_id: - description: 关联的 DNS ID - type: integer - domains: - items: - type: string - type: array - id: - type: integer - key: - description: 私钥内容 - type: string - type: - description: 证书类型 (P256, P384, 2048, 4096) - type: string - updated_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - user: - $ref: '#/definitions/models.CertUser' - user_id: - description: 关联的 ACME 用户 ID - type: integer - website: - $ref: '#/definitions/models.Website' - website_id: - description: 关联的网站 ID - type: integer - type: object - models.CertDNS: - properties: - created_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - dns_param: - $ref: '#/definitions/acme.DNSParam' - id: - type: integer - name: - description: 备注名称 - type: string - type: - description: DNS 提供商 (dnspod, tencent, aliyun, cloudflare) - type: string - updated_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - type: object - models.CertUser: - properties: - ca: - description: CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass) - type: string - created_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - email: - type: string - hmac_encoded: - type: string - id: - type: integer - key_type: - type: string - kid: - type: string - private_key: - type: string - updated_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - type: object - models.Website: - properties: - cert: - $ref: '#/definitions/models.Cert' - created_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - id: - type: integer - name: - type: string - path: - type: string - php: - type: integer - remark: - type: string - ssl: - type: boolean - status: - type: boolean - updated_at: - $ref: '#/definitions/github_com_goravel_framework_support_carbon.DateTime' - type: object - requests.Add: - properties: - db: - type: boolean - db_name: - type: string - db_password: - type: string - db_type: - type: string - db_user: - type: string - domains: - items: - type: string - type: array - name: - type: string - path: - type: string - php: - type: string - ports: - items: - type: integer - type: array - type: object - requests.Archive: - properties: - file: - type: string - paths: - items: - type: string - type: array - type: object - requests.CertDeploy: - properties: - id: - type: integer - website_id: - type: integer - type: object - requests.CertStore: - properties: - auto_renew: - type: boolean - dns_id: - type: integer - domains: - items: - type: string - type: array - type: - type: string - user_id: - type: integer - website_id: - type: integer - type: object - requests.CertUpdate: - properties: - auto_renew: - type: boolean - dns_id: - type: integer - domains: - items: - type: string - type: array - id: - type: integer - type: - type: string - user_id: - type: integer - website_id: - type: integer - type: object - requests.ContainerCreate: - properties: - auto_remove: - type: boolean - command: - items: - type: string - type: array - cpu_shares: - type: integer - cpus: - type: integer - entrypoint: - items: - type: string - type: array - env: - items: - $ref: '#/definitions/types.KV' - type: array - image: - type: string - labels: - items: - $ref: '#/definitions/types.KV' - type: array - memory: - type: integer - name: - type: string - network: - type: string - open_stdin: - type: boolean - ports: - items: - $ref: '#/definitions/types.ContainerPort' - type: array - privileged: - type: boolean - publish_all_ports: - type: boolean - restart_policy: - type: string - tty: - type: boolean - volumes: - items: - $ref: '#/definitions/types.ContainerVolume' - type: array - type: object - requests.ContainerRename: - properties: - id: - type: string - name: - type: string - type: object - requests.Copy: - properties: - source: - type: string - target: - type: string - type: object - requests.Create: - properties: - auth_user: - type: string - comment: - type: string - hosts_allow: - type: string - name: - type: string - path: - type: string - secret: - type: string - type: object - requests.DNSStore: - properties: - data: - $ref: '#/definitions/acme.DNSParam' - name: - type: string - type: - type: string - type: object - requests.DNSUpdate: - properties: - data: - $ref: '#/definitions/acme.DNSParam' - id: - type: integer - name: - type: string - type: - type: string - type: object - requests.Delete: - properties: - db: - type: boolean - id: - type: integer - path: - type: boolean - type: object - requests.DeleteBackup: - properties: - name: - type: string - type: object - requests.Exist: - properties: - path: - type: string - type: object - requests.Https: - properties: - cert: - type: string - https: - type: boolean - key: - type: string - type: object - requests.ImagePull: - properties: - auth: - type: boolean - name: - type: string - password: - type: string - username: - type: string - type: object - requests.Login: - properties: - password: - type: string - username: - type: string - type: object - requests.Move: - properties: - source: - type: string - target: - type: string - type: object - requests.NetworkConnectDisConnect: - properties: - container: - type: string - network: - type: string - type: object - requests.NetworkCreate: - properties: - driver: - type: string - ipv4: - $ref: '#/definitions/types.ContainerNetwork' - ipv6: - $ref: '#/definitions/types.ContainerNetwork' - labels: - items: - $ref: '#/definitions/types.KV' - type: array - name: - type: string - options: - items: - $ref: '#/definitions/types.KV' - type: array - type: object - requests.NotExist: - properties: - path: - type: string - type: object - requests.Obtain: - properties: - id: - type: integer - type: object - requests.Permission: - properties: - group: - type: string - mode: - type: string - owner: - type: string - path: - type: string - type: object - requests.Renew: - properties: - id: - type: integer - type: object - requests.Save: - properties: - content: - type: string - path: - type: string - type: object - requests.SaveConfig: - properties: - domains: - items: - type: string - type: array - hsts: - type: boolean - http_redirect: - type: boolean - id: - type: integer - index: - type: string - open_basedir: - type: boolean - path: - type: string - php: - type: integer - ports: - items: - type: integer - type: array - raw: - type: string - rewrite: - type: string - root: - type: string - ssl: - type: boolean - ssl_certificate: - type: string - ssl_certificate_key: - type: string - tls_ports: - items: - type: integer - type: array - waf: - type: boolean - waf_cache: - type: string - waf_cc_deny: - type: string - waf_mode: - type: string - type: object - requests.Search: - properties: - keyword: - type: string - path: - type: string - type: object - requests.UnArchive: - properties: - file: - type: string - path: - type: string - type: object - requests.UpdateRegistryConfig: - properties: - config: - type: string - type: object - requests.UpdateStorageConfig: - properties: - config: - type: string - type: object - requests.UserStore: - properties: - ca: - type: string - email: - type: string - hmac_encoded: - type: string - key_type: - type: string - kid: - type: string - type: object - requests.UserUpdate: - properties: - ca: - type: string - email: - type: string - hmac_encoded: - type: string - id: - type: integer - key_type: - type: string - kid: - type: string - type: object - requests.VolumeCreate: - properties: - driver: - type: string - labels: - items: - $ref: '#/definitions/types.KV' - type: array - name: - type: string - options: - items: - $ref: '#/definitions/types.KV' - type: array - type: object - types.BackupFile: - properties: - name: - type: string - size: - type: string - type: object - types.ContainerNetwork: - properties: - enabled: - type: boolean - gateway: - type: string - ip_range: - type: string - subnet: - type: string - type: object - types.ContainerPort: - properties: - container_end: - type: integer - container_start: - type: integer - host: - type: string - host_end: - type: integer - host_start: - type: integer - protocol: - type: string - type: object - types.ContainerVolume: - properties: - container: - type: string - host: - type: string - mode: - type: string - type: object - types.KV: - properties: - key: - type: string - value: - type: string - type: object - types.WebsiteAdd: - properties: - db: - type: boolean - db_name: - type: string - db_password: - type: string - db_type: - type: string - db_user: - type: string - domains: - items: - type: string - type: array - name: - type: string - path: - type: string - php: - type: string - ports: - items: - type: integer - type: array - remark: - type: string - ssl: - type: boolean - status: - type: boolean - type: object info: contact: email: admin@haozi.net @@ -697,1967 +30,175 @@ info: title: 耗子面板 API version: "2" paths: - /panel/cert/algorithms: - get: - description: 获取面板证书管理支持的算法列表 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取算法列表 - tags: - - TLS证书 - /panel/cert/caProviders: - get: - description: 获取面板证书管理支持的 CA 提供商 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取 CA 提供商 - tags: - - TLS证书 - /panel/cert/certs: - get: - description: 获取面板证书管理的证书列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取证书列表 - tags: - - TLS证书 - post: - consumes: - - application/json - description: 添加证书到面板证书管理 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.CertStore' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加证书 - tags: - - TLS证书 - /panel/cert/certs/{id}: - delete: - consumes: - - application/json - description: 删除面板证书管理的证书 - parameters: - - description: 证书 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除证书 - tags: - - TLS证书 - get: - description: 获取面板证书管理的证书 - parameters: - - description: 证书 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - $ref: '#/definitions/models.Cert' - type: object - security: - - BearerToken: [] - summary: 获取证书 - tags: - - TLS证书 - put: - consumes: - - application/json - description: 更新面板证书管理的证书 - parameters: - - description: 证书 ID - in: path - name: id - required: true - type: integer - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.CertUpdate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新证书 - tags: - - TLS证书 - /panel/cert/deploy: - post: - consumes: - - application/json - description: 部署面板证书管理的证书 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.CertDeploy' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 部署证书 - tags: - - TLS证书 - /panel/cert/dns: - get: - description: 获取面板证书管理的 DNS 接口列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取 DNS 接口列表 - tags: - - TLS证书 - post: - consumes: - - application/json - description: 添加 DNS 接口到面板证书管理 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.DNSStore' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加 DNS 接口 - tags: - - TLS证书 - /panel/cert/dns/{id}: - delete: - consumes: - - application/json - description: 删除面板证书管理的 DNS 接口 - parameters: - - description: DNS 接口 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除 DNS 接口 - tags: - - TLS证书 - get: - description: 获取面板证书管理的 DNS 接口 - parameters: - - description: DNS 接口 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - $ref: '#/definitions/models.CertDNS' - type: object - security: - - BearerToken: [] - summary: 获取 DNS 接口 - tags: - - TLS证书 - put: - consumes: - - application/json - description: 更新面板证书管理的 DNS 接口 - parameters: - - description: DNS 接口 ID - in: path - name: id - required: true - type: integer - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.DNSUpdate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新 DNS 接口 - tags: - - TLS证书 - /panel/cert/dnsProviders: - get: - description: 获取面板证书管理支持的 DNS 提供商 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取 DNS 提供商 - tags: - - TLS证书 - /panel/cert/manualDNS: - post: - consumes: - - application/json - description: 获取签发证书所需的 DNS 记录 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Obtain' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - items: - $ref: '#/definitions/acme.DNSRecord' - type: array - type: object - security: - - BearerToken: [] - summary: 获取手动 DNS 记录 - tags: - - TLS证书 - /panel/cert/obtain: - post: - consumes: - - application/json - description: 签发面板证书管理的证书 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Obtain' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 签发证书 - tags: - - TLS证书 - /panel/cert/renew: - post: - consumes: - - application/json - description: 续签面板证书管理的证书 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Renew' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 续签证书 - tags: - - TLS证书 - /panel/cert/users: - get: - description: 获取面板证书管理的 ACME 用户列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取用户列表 - tags: - - TLS证书 - post: - consumes: - - application/json - description: 添加 ACME 用户到面板证书管理 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UserStore' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加 ACME 用户 - tags: - - TLS证书 - /panel/cert/users/{id}: - delete: - consumes: - - application/json - description: 删除面板证书管理的 ACME 用户 - parameters: - - description: 用户 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除 ACME 用户 - tags: - - TLS证书 - get: - description: 获取面板证书管理的 ACME 用户 - parameters: - - description: 用户 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - $ref: '#/definitions/models.CertUser' - type: object - security: - - BearerToken: [] - summary: 获取 ACME 用户 - tags: - - TLS证书 - put: - consumes: - - application/json - description: 更新面板证书管理的 ACME 用户 - parameters: - - description: 用户 ID - in: path - name: id - required: true - type: integer - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UserUpdate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新 ACME 用户 - tags: - - TLS证书 - /panel/container/create: - post: - consumes: - - application/json - description: 创建一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.ContainerCreate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建容器 - tags: - - 容器 - /panel/container/exist: - get: - description: 检查一个容器是否存在 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查容器是否存在 - tags: - - 容器 - /panel/container/image/exist: - get: - description: 检查一个镜像是否存在 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查镜像是否存在 - tags: - - 容器 - /panel/container/image/inspect: - get: - description: 查看一个镜像的详细信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看镜像 - tags: - - 容器 - /panel/container/image/list: - get: - description: 获取所有镜像列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取镜像列表 - tags: - - 容器 - /panel/container/image/prune: - post: - description: 清理无用的镜像 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清理镜像 - tags: - - 容器 - /panel/container/image/pull: - post: - consumes: - - application/json - description: 拉取一个镜像 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.ImagePull' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 拉取镜像 - tags: - - 容器 - /panel/container/image/remove: - post: - description: 删除一个镜像 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除镜像 - tags: - - 容器 - /panel/container/inspect: - get: - description: 查看一个容器的详细信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看容器 - tags: - - 容器 - /panel/container/kill: - post: - description: 杀死一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 杀死容器 - tags: - - 容器 - /panel/container/list: - get: - description: 获取所有容器列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取容器列表 - tags: - - 容器 - /panel/container/logs: - get: - description: 查看一个容器的日志 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看容器日志 - tags: - - 容器 - /panel/container/network/connect: - post: - consumes: - - application/json - description: 连接一个容器到一个网络 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NetworkConnectDisConnect' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 连接容器到网络 - tags: - - 容器 - /panel/container/network/create: - post: - consumes: - - application/json - description: 创建一个网络 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NetworkCreate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建网络 - tags: - - 容器 - /panel/container/network/disconnect: - post: - consumes: - - application/json - description: 从一个网络断开一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NetworkConnectDisConnect' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 从网络断开容器 - tags: - - 容器 - /panel/container/network/exist: - get: - description: 检查一个网络是否存在 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查网络是否存在 - tags: - - 容器 - /panel/container/network/inspect: - get: - description: 查看一个网络的详细信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看网络 - tags: - - 容器 - /panel/container/network/list: - get: - description: 获取所有网络列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取网络列表 - tags: - - 容器 - /panel/container/network/prune: - post: - description: 清理无用的网络 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清理网络 - tags: - - 容器 - /panel/container/network/remove: - post: - description: 删除一个网络 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除网络 - tags: - - 容器 - /panel/container/prune: - post: - description: 清理无用的容器 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清理容器 - tags: - - 容器 - /panel/container/remove: - post: - description: 删除一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除容器 - tags: - - 容器 - /panel/container/rename: - post: - description: 重命名一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.ContainerRename' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重命名容器 - tags: - - 容器 - /panel/container/restart: - post: - description: 重启一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重启容器 - tags: - - 容器 - /panel/container/search: - get: - description: 根据容器名称搜索容器 - parameters: - - description: 容器名称 - in: query - name: name - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 搜索容器 - tags: - - 容器 - /panel/container/start: - post: - description: 启动一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 启动容器 - tags: - - 容器 - /panel/container/stats: - get: - description: 查看一个容器的状态信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看容器状态 - tags: - - 容器 - /panel/container/stop: - post: - description: 停止一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 停止容器 - tags: - - 容器 - /panel/container/unpause: - post: - description: 取消暂停一个容器 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 取消暂停容器 - tags: - - 容器 - /panel/container/volume/create: - post: - consumes: - - application/json - description: 创建一个卷 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.VolumeCreate' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建卷 - tags: - - 容器 - /panel/container/volume/exist: - get: - description: 检查一个卷是否存在 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查卷是否存在 - tags: - - 容器 - /panel/container/volume/inspect: - get: - description: 查看一个卷的详细信息 - parameters: - - in: query - name: id - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 查看卷 - tags: - - 容器 - /panel/container/volume/list: - get: - description: 获取所有卷列表 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取卷列表 - tags: - - 容器 - /panel/container/volume/prune: - post: - description: 清理无用的卷 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清理卷 - tags: - - 容器 - /panel/container/volume/remove: - post: - description: 删除一个卷 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_container.ID' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除卷 - tags: - - 容器 - /panel/file/archive: - post: - consumes: - - application/json - description: 压缩文件/目录到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Archive' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 压缩文件/目录 - tags: - - 文件 - /panel/file/content: + /info/checkUpdate: get: consumes: - application/json - description: 获取给定路径的文件内容 - parameters: - - in: query - name: path - type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取文件内容 + $ref: '#/definitions/service.SuccessResponse' + summary: 检查更新 tags: - - 文件 - /panel/file/copy: - post: - consumes: - - application/json - description: 复制文件/目录到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Copy' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 复制文件/目录 - tags: - - 文件 - /panel/file/create: - post: - consumes: - - application/json - description: 创建文件/目录到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NotExist' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建文件/目录 - tags: - - 文件 - /panel/file/delete: - post: - consumes: - - application/json - description: 删除给定路径的文件/目录 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Exist' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除文件/目录 - tags: - - 文件 - /panel/file/download: + - 信息服务 + /info/countInfo: get: consumes: - application/json - description: 下载给定路径的文件 - parameters: - - in: query - name: path - type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 下载文件 + $ref: '#/definitions/service.SuccessResponse' + summary: 统计信息 tags: - - 文件 - /panel/file/info: + - 信息服务 + /info/homePlugins: get: consumes: - application/json - description: 获取给定路径的文件/目录信息 - parameters: - - in: query - name: path - type: string produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取文件/目录信息 + $ref: '#/definitions/service.SuccessResponse' + summary: 首页插件 tags: - - 文件 - /panel/file/list: + - 信息服务 + /info/installedDbAndPhp: get: consumes: - application/json - description: 获取给定路径的文件/目录列表 - parameters: - - in: query - name: path - type: string - - in: query - name: limit - type: integer - - in: query - name: page - type: integer produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - summary: 获取文件/目录列表 + $ref: '#/definitions/service.SuccessResponse' + summary: 已安装的数据库和PHP tags: - - 文件 - /panel/file/move: - post: - consumes: - - application/json - description: 移动文件/目录到给定路径,等效于重命名 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Move' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 移动文件/目录 - tags: - - 文件 - /panel/file/permission: - post: - consumes: - - application/json - description: 修改给定路径的文件/目录权限 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Permission' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 修改文件/目录权限 - tags: - - 文件 - /panel/file/remoteDownload: - post: - consumes: - - application/json - description: 下载远程文件到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.NotExist' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 下载远程文件 - tags: - - 文件 - /panel/file/save: - post: - consumes: - - application/json - description: 保存给定路径的文件内容 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Save' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存文件内容 - tags: - - 文件 - /panel/file/search: - post: - consumes: - - application/json - description: 通过关键词搜索给定路径的文件/目录 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Search' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - summary: 搜索文件/目录 - tags: - - 文件 - /panel/file/unArchive: - post: - consumes: - - application/json - description: 解压文件/目录到给定路径 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UnArchive' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 解压文件/目录 - tags: - - 文件 - /panel/file/upload: - post: - consumes: - - application/json - description: 上传文件到给定路径 - parameters: - - description: file - in: formData - name: file - required: true - type: file - - description: path - in: formData - name: path - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 上传文件 - tags: - - 文件 - /panel/plugin/install: - post: - parameters: - - description: request - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 安装插件 - tags: - - 插件 - /panel/plugin/isInstalled: + - 信息服务 + /info/nowMonitor: get: - parameters: - - description: request - in: query - name: slug - required: true - type: string + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 检查插件是否已安装 + $ref: '#/definitions/service.SuccessResponse' + summary: 实时监控 tags: - - 插件 - /panel/plugin/list: + - 信息服务 + /info/panel: get: + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 插件列表 + $ref: '#/definitions/service.SuccessResponse' + summary: 面板信息 tags: - - 插件 - /panel/plugin/uninstall: - post: - parameters: - - description: request - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 卸载插件 - tags: - - 插件 - /panel/plugin/update: - post: - parameters: - - description: request - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新插件 - tags: - - 插件 - /panel/plugin/updateShow: - post: - parameters: - - description: request - in: query - name: slug - required: true - type: string - - description: request - in: query - name: show - required: true - type: boolean - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新插件首页显示状态 - tags: - - 插件 - /panel/setting/https: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取面板 HTTPS 设置 - tags: - - 面板设置 + - 信息服务 + /info/restart: post: consumes: - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Https' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新面板 HTTPS 设置 + $ref: '#/definitions/service.SuccessResponse' + summary: 重启面板 tags: - - 面板设置 - /panel/setting/list: + - 信息服务 + /info/systemInfo: get: + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 设置列表 + $ref: '#/definitions/service.SuccessResponse' + summary: 系统信息 tags: - - 面板设置 - /panel/setting/update: + - 信息服务 + /info/update: post: consumes: - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_setting.Update' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新设置 + $ref: '#/definitions/service.SuccessResponse' + summary: 更新面板 tags: - - 面板设置 - /panel/system/service/disable: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 禁用服务 - tags: - - 系统 - /panel/system/service/enable: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 启用服务 - tags: - - 系统 - /panel/system/service/isEnabled: + - 信息服务 + /info/updateInfo: get: - parameters: - - description: request - in: query - name: data - required: true - type: string + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 是否启用服务 + $ref: '#/definitions/service.SuccessResponse' + summary: 版本更新信息 tags: - - 系统 - /panel/system/service/reload: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重载服务 - tags: - - 系统 - /panel/system/service/restart: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重启服务 - tags: - - 系统 - /panel/system/service/start: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 启动服务 - tags: - - 系统 - /panel/system/service/status: + - 信息服务 + /user/info/{id}: get: - parameters: - - description: request - in: query - name: data - required: true - type: string + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 服务状态 - tags: - - 系统 - /panel/system/service/stop: - post: - parameters: - - description: request - in: body - name: data - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 停止服务 - tags: - - 系统 - /panel/user/info: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] + $ref: '#/definitions/service.SuccessResponse' summary: 用户信息 tags: - - 用户鉴权 - /panel/user/login: + - 用户服务 + /user/isLogin: + get: + consumes: + - application/json + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/service.SuccessResponse' + summary: 是否登录 + tags: + - 用户服务 + /user/login: post: consumes: - application/json @@ -2667,1029 +208,31 @@ paths: name: data required: true schema: - $ref: '#/definitions/requests.Login' + $ref: '#/definitions/request.UserLogin' produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - "403": - description: 用户名或密码错误 - schema: - $ref: '#/definitions/controllers.ErrorResponse' - "500": - description: 系统内部错误 - schema: - $ref: '#/definitions/controllers.ErrorResponse' + $ref: '#/definitions/service.SuccessResponse' summary: 登录 tags: - - 用户鉴权 - /panel/user/logout: + - 用户服务 + /user/logout: post: + consumes: + - application/json produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] + $ref: '#/definitions/service.SuccessResponse' summary: 登出 tags: - - 用户鉴权 - /panel/website/backupList: - get: - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - items: - $ref: '#/definitions/types.BackupFile' - type: array - type: object - security: - - BearerToken: [] - summary: 获取网站备份列表 - tags: - - 网站 - /panel/website/defaultConfig: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - additionalProperties: - type: string - type: object - type: object - security: - - BearerToken: [] - summary: 获取默认配置 - tags: - - 网站 - post: - consumes: - - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - additionalProperties: - type: string - type: object - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存默认配置 - tags: - - 网站 - /panel/website/deleteBackup: - delete: - consumes: - - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.DeleteBackup' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除网站备份 - tags: - - 网站 - /panel/website/uploadBackup: - put: - consumes: - - application/json - parameters: - - description: 备份文件 - in: formData - name: file - required: true - type: file - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 上传网站备份 - tags: - - 网站 - /panel/websites: - get: - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取网站列表 - tags: - - 网站 - post: - consumes: - - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Add' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加网站 - tags: - - 网站 - /panel/websites/{id}/config: - get: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/controllers.SuccessResponse' - - properties: - data: - $ref: '#/definitions/types.WebsiteAdd' - type: object - security: - - BearerToken: [] - summary: 获取网站配置 - tags: - - 网站 - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.SaveConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存网站配置 - tags: - - 网站 - /panel/websites/{id}/createBackup: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 创建网站备份 - tags: - - 网站 - /panel/websites/{id}/log: - delete: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清空网站日志 - tags: - - 网站 - /panel/websites/{id}/resetConfig: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 重置网站配置 - tags: - - 网站 - /panel/websites/{id}/restoreBackup: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 还原网站备份 - tags: - - 网站 - /panel/websites/{id}/status: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取网站状态 - tags: - - 网站 - /panel/websites/{id}/updateRemark: - post: - consumes: - - application/json - parameters: - - description: 网站 ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新网站备注 - tags: - - 网站 - /panel/websites/delete: - post: - consumes: - - application/json - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Delete' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除网站 - tags: - - 网站 - /plugins/frp/config: - get: - description: 获取 Frp 配置 - parameters: - - description: 服务 - in: query - name: service - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-Frp - post: - description: 更新 Frp 配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_frp.UpdateConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新配置 - tags: - - 插件-Frp - /plugins/gitea/config: - get: - description: 获取 Gitea 配置 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-Gitea - post: - description: 更新 Gitea 配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_gitea.UpdateConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新配置 - tags: - - 插件-Gitea - /plugins/openresty/clearErrorLog: - post: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清空错误日志 - tags: - - 插件-OpenResty - /plugins/openresty/config: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-OpenResty - post: - parameters: - - description: 配置 - in: body - name: config - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存配置 - tags: - - 插件-OpenResty - /plugins/openresty/errorLog: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取错误日志 - tags: - - 插件-OpenResty - /plugins/openresty/load: - get: - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取负载状态 - tags: - - 插件-OpenResty - /plugins/php/{version}/clearErrorLog: - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清空错误日志 - tags: - - 插件-PHP - /plugins/php/{version}/clearSlowLog: - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 清空慢日志 - tags: - - 插件-PHP - /plugins/php/{version}/config: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-PHP - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - - description: 配置 - in: body - name: config - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存配置 - tags: - - 插件-PHP - /plugins/php/{version}/errorLog: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取错误日志 - tags: - - 插件-PHP - /plugins/php/{version}/extensions: - delete: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - - description: slug - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 卸载扩展 - tags: - - 插件-PHP - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取扩展列表 - tags: - - 插件-PHP - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - - description: slug - in: query - name: slug - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 安装扩展 - tags: - - 插件-PHP - /plugins/php/{version}/fpmConfig: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取 FPM 配置 - tags: - - 插件-PHP - post: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - - description: 配置 - in: body - name: config - required: true - schema: - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 保存 FPM 配置 - tags: - - 插件-PHP - /plugins/php/{version}/load: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取负载状态 - tags: - - 插件-PHP - /plugins/php/{version}/slowLog: - get: - parameters: - - description: PHP 版本 - in: path - name: version - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取慢日志 - tags: - - 插件-PHP - /plugins/podman/registryConfig: - get: - description: 获取 Podman 注册表配置 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取注册表配置 - tags: - - 插件-Podman - post: - description: 更新 Podman 注册表配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UpdateRegistryConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新注册表配置 - tags: - - 插件-Podman - /plugins/podman/storageConfig: - get: - description: 获取 Podman 存储配置 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取存储配置 - tags: - - 插件-Podman - post: - description: 更新 Podman 存储配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.UpdateStorageConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新存储配置 - tags: - - 插件-Podman - /plugins/rsync/config: - get: - description: 获取 Rsync 配置 - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 获取配置 - tags: - - 插件-Rsync - post: - description: 更新 Rsync 配置 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.UpdateConfig' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新配置 - tags: - - 插件-Rsync - /plugins/rsync/modules: - get: - description: 列出所有 Rsync 模块 - parameters: - - in: query - name: limit - type: integer - - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 列出模块 - tags: - - 插件-Rsync - post: - description: 添加 Rsync 模块 - parameters: - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/requests.Create' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 添加模块 - tags: - - 插件-Rsync - /plugins/rsync/modules/{name}: - delete: - description: 删除 Rsync 模块 - parameters: - - description: 模块名称 - in: path - name: name - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 删除模块 - tags: - - 插件-Rsync - post: - description: 更新 Rsync 模块 - parameters: - - description: 模块名称 - in: path - name: name - required: true - type: string - - description: request - in: body - name: data - required: true - schema: - $ref: '#/definitions/github_com_TheTNB_panel_app_http_requests_plugins_rsync.Update' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/controllers.SuccessResponse' - security: - - BearerToken: [] - summary: 更新模块 - tags: - - 插件-Rsync - /swagger: - get: - description: Swagger UI - responses: - "200": - description: OK - "500": - description: Internal Server Error - summary: Swagger UI - tags: - - Swagger + - 用户服务 securityDefinitions: BearerToken: in: header diff --git a/embed/frontend/.gitignore b/embed/frontend/.gitignore deleted file mode 100644 index f59ec20a..00000000 --- a/embed/frontend/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/go.mod b/go.mod index c89cc5dc..46260c79 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,30 @@ -module github.com/TheTNB/panel/v2 +module github.com/TheTNB/panel -go 1.22 +go 1.23 require ( - github.com/docker/docker v27.1.2+incompatible + github.com/beevik/ntp v1.4.3 + github.com/docker/docker v27.2.1+incompatible github.com/docker/go-connections v0.5.0 - github.com/gin-contrib/static v1.1.2 + github.com/glebarez/sqlite v1.11.0 + github.com/go-chi/chi/v5 v5.1.0 github.com/go-gormigrate/gormigrate/v2 v2.1.2 - github.com/go-resty/resty/v2 v2.14.0 + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 + github.com/go-playground/validator/v10 v10.22.1 + github.com/go-rat/chix v1.1.3 + github.com/go-rat/gormstore v1.0.5 + github.com/go-rat/sessions v1.0.9 + github.com/go-rat/utils v1.0.3 + github.com/go-resty/resty/v2 v2.15.0 github.com/go-sql-driver/mysql v1.8.1 - github.com/gookit/validate v1.5.2 - github.com/goravel/framework v1.14.1-0.20240728082300-b71cfeb464af - github.com/goravel/gin v1.2.3-0.20240714200024-34029bdef5d1 + github.com/golang-module/carbon/v2 v2.3.12 + github.com/gookit/color v1.5.4 github.com/gorilla/websocket v1.5.3 github.com/hashicorp/go-version v1.7.0 + github.com/knadh/koanf/parsers/yaml v0.1.0 + github.com/knadh/koanf/providers/file v1.1.0 + github.com/knadh/koanf/v2 v2.1.1 github.com/lib/pq v1.10.9 github.com/libdns/alidns v1.0.3 github.com/libdns/cloudflare v0.1.1 @@ -21,207 +32,106 @@ require ( github.com/libdns/libdns v0.2.2 github.com/libdns/tencentcloud v1.0.0 github.com/mholt/acmez/v2 v2.0.2 - github.com/mholt/archiver/v3 v3.5.1 - github.com/shirou/gopsutil v3.21.11+incompatible + github.com/mholt/archiver/v4 v4.0.0-alpha.8 + github.com/sethvargo/go-limiter v1.0.0 + github.com/shirou/gopsutil v2.21.11+incompatible github.com/spf13/cast v1.7.0 github.com/stretchr/testify v1.9.0 github.com/swaggo/http-swagger/v2 v2.0.2 github.com/swaggo/swag v1.16.3 + github.com/urfave/cli/v2 v2.27.4 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.26.0 - golang.org/x/net v0.28.0 - gorm.io/gorm v1.25.11 + golang.org/x/crypto v0.27.0 + golang.org/x/net v0.29.0 + gorm.io/gorm v1.25.12 ) require ( - atomicgo.dev/cursor v0.2.0 // indirect - atomicgo.dev/keyboard v0.2.9 // indirect - atomicgo.dev/schedule v0.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.16 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/catppuccin/go v0.2.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/charmbracelet/bubbles v0.18.0 // indirect - github.com/charmbracelet/bubbletea v0.26.6 // indirect - github.com/charmbracelet/huh v0.5.2 // indirect - github.com/charmbracelet/huh/spinner v0.0.0-20240725212135-67d4a4354ed1 // indirect - github.com/charmbracelet/lipgloss v0.12.1 // indirect - github.com/charmbracelet/x/ansi v0.1.4 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/input v0.1.3 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.2 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/Microsoft/go-winio v0.4.14 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/bodgit/plumbing v1.2.0 // indirect + github.com/bodgit/sevenzip v1.3.0 // indirect + github.com/bodgit/windows v1.0.0 // indirect + github.com/connesc/cipherio v0.2.1 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.10.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/glebarez/sqlite v1.11.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/spec v0.20.14 // indirect - github.com/go-openapi/swag v0.22.7 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-rat/securecookie v1.0.1 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/golang-migrate/migrate/v4 v4.17.1 // indirect - github.com/golang-module/carbon/v2 v2.3.12 // indirect - github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect - github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/google/wire v0.6.0 // indirect - github.com/gookit/color v1.5.4 // indirect - github.com/gookit/filter v1.2.1 // indirect - github.com/gookit/goutil v0.6.15 // indirect - github.com/goravel/file-rotatelogs/v2 v2.4.2 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jaevor/go-nanoid v1.4.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.7 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/pgzip v1.2.6 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lithammer/fuzzysearch v1.1.8 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/microsoft/go-mssqldb v1.6.0 // indirect - github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect - github.com/nwaples/rardecode v1.1.0 // indirect + github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc5 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/pierrec/lz4/v4 v4.1.16 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/pterm/pterm v0.12.79 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/rotisserie/eris v0.5.4 // indirect - github.com/rs/cors v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/samber/do/v2 v2.0.0-beta.7 // indirect - github.com/samber/go-type-to-string v1.4.0 // indirect - github.com/savioxavier/termlink v1.3.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.6.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - github.com/ulikunitz/xz v0.5.11 // indirect - github.com/unrolled/secure v1.15.0 // indirect - github.com/urfave/cli/v2 v2.27.3 // indirect - github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.22.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.uber.org/atomic v1.11.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + go4.org v0.0.0-20200411211856-f5505b9728dd // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.23.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/mysql v1.5.7 // indirect - gorm.io/driver/postgres v1.5.9 // indirect - gorm.io/driver/sqlserver v1.5.3 // indirect - gorm.io/plugin/dbresolver v1.5.2 // indirect - gotest.tools/v3 v3.5.0 // indirect - modernc.org/libc v1.37.6 // indirect + gotest.tools/v3 v3.5.1 // indirect + modernc.org/libc v1.60.1 // indirect modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.7.2 // indirect - modernc.org/sqlite v1.28.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.32.0 // indirect ) - -// The current latest version of github.com/mholt/archiver/v3 (v3.5.1) suffers from CVE-2024-0406. -// There is currently a PR in place to resolve it (https://github.com/mholt/archiver/pull/396), -// but it has not had much attention recently. -// Just replace our usage of github.com/mholt/archiver/v3 with github.com/anchore/archiver/v3 (v3.5.2) -// so static vulnerability scanners will be happy. -// This version (probably) fixes CVE-2024-0406, but we are also unaffected by that vulnerability anyway, -// as we do not use [(*archiver.Tar).Unarchive()], so it doesn't really matter. -// What is important, though, is the code changes between github.com/mholt/archiver/v3 v3.5.1 -// and github.com/anchore/archiver/v3 v3.5.2 only touch the [(*archiver.Tar).Unarchive()] path, -// and nothing we use. See https://github.com/mholt/archiver/compare/v3.5.1...anchore:archiver:v3.5.2 -// for more details of the exact difference. -replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2 diff --git a/go.sum b/go.sum index d9bedbb6..ba651573 100644 --- a/go.sum +++ b/go.sum @@ -1,142 +1,61 @@ -atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= -atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= -atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= -atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= -atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= -atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= -atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= -atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2 h1:t5+QXLCK9SVi0PPdaY0PrFvYUo24KwA0QwxnaHRSVd4= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1 h1:LNHhpdK7hzUcx/k1LIcuh5k7k1LGIWLQfCjaneSj7Fc= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest/adal v0.9.16 h1:P8An8Z9rH1ldbOLdFpxYorgOt2sywL9V24dAwWHPuGc= -github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= -github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= -github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= -github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= -github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= -github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= -github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= -github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= -github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= -github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/anchore/archiver/v3 v3.5.2 h1:Bjemm2NzuRhmHy3m0lRe5tNoClB9A4zYyDV58PaB6aA= -github.com/anchore/archiver/v3 v3.5.2/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= -github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= -github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= -github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= +github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= +github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM= +github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8= +github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY= +github.com/bodgit/sevenzip v1.3.0/go.mod h1:omwNcgZTEooWM8gA/IJ2Nk/+ZQ94+GsytRzOJJ8FBlM= +github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA= +github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= -github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= -github.com/charmbracelet/huh v0.5.2 h1:ofeNkJ4iaFnzv46Njhx896DzLUe/j0L2QAf8znwzX4c= -github.com/charmbracelet/huh v0.5.2/go.mod h1:Sf7dY0oAn6N/e3sXJFtFX9hdQLrUdO3z7AYollG9bAM= -github.com/charmbracelet/huh/spinner v0.0.0-20240725212135-67d4a4354ed1 h1:IjNcc7cCYR0ymVfy4dWvBHE6VSqfpcvHRfWpCWcun0g= -github.com/charmbracelet/huh/spinner v0.0.0-20240725212135-67d4a4354ed1/go.mod h1:9VssyY5pUozMRmDYlLYV20QMMcA2sHg3qnaB6PvdIm8= -github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= -github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= -github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= -github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= -github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= -github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg= -github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= -github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= +github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= -github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY= -github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -146,14 +65,8 @@ github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj6 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -162,179 +75,141 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4= -github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9zTgHSBoOOZ4CY= github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= -github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= -github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8= -github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU= -github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-rat/chix v1.1.3 h1:NeDbmA3OTfhEeizn528dz8z8/lBcsziftgtSarYn7Vo= +github.com/go-rat/chix v1.1.3/go.mod h1:XeC0hmaAXlPB/V8g0QZiIAgnAJO70ZeT+k1Q4Zh9qeI= +github.com/go-rat/gormstore v1.0.5 h1:76umSO6n+zoLnxVryX+6aUu6rIQLveGO8fCu9d5GzTA= +github.com/go-rat/gormstore v1.0.5/go.mod h1:xG/n+Du8baWf7ptiafiHLWBZWVXrYIPC42CsoEbfr4M= +github.com/go-rat/securecookie v1.0.1 h1:HW0fpKmB+FjJzXTw8ABOwBJ+XrPmRBSZqHhmrv86lBo= +github.com/go-rat/securecookie v1.0.1/go.mod h1:tP/ObWYyjmcpabQ7WTon/i2lBSip/Aolliw2llXuPDU= +github.com/go-rat/sessions v1.0.9 h1:zysrHceTFqyc0qUWn5giLGdbfr/DYHXvFECDBSyv9Gk= +github.com/go-rat/sessions v1.0.9/go.mod h1:Ray/GCbuhm4U9xpjFFSCfOTCEn91puEhAXX5creHE9g= +github.com/go-rat/utils v1.0.3 h1:SqH/O0KYq4SBv8naSAjJA4hC45/d8NLNrTCONfqjbFM= +github.com/go-rat/utils v1.0.3/go.mod h1:4WNPlrF57KmeGZN3HOeBgdBVLJL3xgba4QRP/t6pKrE= +github.com/go-resty/resty/v2 v2.15.0 h1:clPQLZ2x9h4yGY81IzpMPnty+xoGyFaDg0XMkCsHf90= +github.com/go-resty/resty/v2 v2.15.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= -github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang-module/carbon/v2 v2.3.12 h1:VC1DwN1kBwJkh5MjXmTFryjs5g4CWyoM8HAHffZPX/k= github.com/golang-module/carbon/v2 v2.3.12/go.mod h1:HNsedGzXGuNciZImYP2OMnpiwq/vhIstR/vn45ib5cI= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= -github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= -github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= -github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= -github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= -github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/gookit/filter v1.2.1 h1:37XivkBm2E5qe1KaGdJ5ZfF5l9NYdGWfLEeQadJD8O4= -github.com/gookit/filter v1.2.1/go.mod h1:rxynQFr793x+XDwnRmJFEb53zDw0Zqx3OD7TXWoR9mQ= -github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo= -github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY= -github.com/gookit/validate v1.5.2 h1:i5I2OQ7WYHFRPRATGu9QarR9snnNHydvwSuHXaRWAV0= -github.com/gookit/validate v1.5.2/go.mod h1:yuPy2WwDlwGRa06fFJ5XIO8QEwhRnTC2LmxmBa5SE14= -github.com/goravel/file-rotatelogs/v2 v2.4.2 h1:g68AzbePXcm0V2CpUMc9j4qVzcDn7+7aoWSjZ51C0m4= -github.com/goravel/file-rotatelogs/v2 v2.4.2/go.mod h1:23VuSW8cBS4ax5cmbV+5AaiLpq25b8UJ96IhbAkdo8I= -github.com/goravel/framework v1.14.1-0.20240728082300-b71cfeb464af h1:ALAKozYauAV6vFwFE7OsM799yUyoJTBGZifWmSoBiVI= -github.com/goravel/framework v1.14.1-0.20240728082300-b71cfeb464af/go.mod h1:ynfAeFhmDC5J67Y6kqw90w/wHNzZvnjfLOkAwr7tPT4= -github.com/goravel/gin v1.2.3-0.20240714200024-34029bdef5d1 h1:3HURtSgEPnX9wy78wx/90cZYBCanEweX+8SpZ5dX0m0= -github.com/goravel/gin v1.2.3-0.20240714200024-34029bdef5d1/go.mod h1:Y2d8KYT/tS5sEMJ3j3ll86kEz2op7vDrbEg6nhURkUQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jaevor/go-nanoid v1.4.0 h1:mPz0oi3CrQyEtRxeRq927HHtZCJAAtZ7zdy7vOkrvWs= +github.com/jaevor/go-nanoid v1.4.0/go.mod h1:GIpPtsvl3eSBsjjIEFQdzzgpi50+Bo1Luk+aYlbJzlc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/file v1.1.0 h1:MTjA+gRrVl1zqgetEAIaXHqYje0XSosxSiMD4/7kz0o= +github.com/knadh/koanf/providers/file v1.1.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -342,12 +217,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= -github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= @@ -362,141 +233,77 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU= github.com/libdns/tencentcloud v1.0.0/go.mod h1:NlCgPumzUsZWSOo1+Q/Hfh8G6TNRAaTUeWQdg6LbtUI= -github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= -github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mholt/acmez/v2 v2.0.2 h1:OmK6xckte2JfKGPz4OAA8aNHTiLvGp8tLzmrd/wfSyw= github.com/mholt/acmez/v2 v2.0.2/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= -github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc= -github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= -github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= -github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= +github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= -github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U= github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= -github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= -github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk= +github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= -github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.16 h1:kQPfno+wyx6C5572ABwV+Uo3pDFzQ7yhyGchSyRda0c= -github.com/pierrec/lz4/v4 v4.1.16/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= -github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= -github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= -github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= -github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= -github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= -github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= -github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= -github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= -github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= -github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/samber/do/v2 v2.0.0-beta.7 h1:tmdLOVSCbTA6uGWLU5poi/nZvMRh5QxXFJ9vHytU+Jk= -github.com/samber/do/v2 v2.0.0-beta.7/go.mod h1:+LpV3vu4L81Q1JMZNSkMvSkW9lt4e5eJoXoZHkeBS4c= -github.com/samber/go-type-to-string v1.4.0 h1:KXphToZgiFdnJQxryU25brhlh/CqY/cwJVeX2rfmow0= -github.com/samber/go-type-to-string v1.4.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU= -github.com/savioxavier/termlink v1.3.0 h1:3Gl4FzQjUyiHzmoEDfmWEhgIwDiJY4poOQHP+k8ReA4= -github.com/savioxavier/termlink v1.3.0/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4= +github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= +github.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U= +github.com/shirou/gopsutil v2.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= @@ -505,97 +312,86 @@ github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= -github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/unrolled/secure v1.15.0 h1:q7x+pdp8jAHnbzxu6UheP8fRlG/rwYTb8TPuQ3rn9Og= -github.com/unrolled/secure v1.15.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= -github.com/urfave/cli/v2 v2.27.3 h1:/POWahRmdh7uztQ3CYnaDddk0Rm90PyOgIxgW2rr41M= -github.com/urfave/cli/v2 v2.27.3/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 h1:R/OBkMoGgfy2fLhs2QhkCI1w4HLEQX92GCcJB6SSdNk= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 h1:Ydage/P0fRrSPpZeCVxzjqGcI6iVmG2xb43+IR8cjqM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= -go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= +go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -604,196 +400,182 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= +google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= -gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= -gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= -gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0= -gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00= -gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= -gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= -gorm.io/plugin/dbresolver v1.5.2 h1:Iut7lW4TXNoVs++I+ra3zxjSxTRj4ocIeFEVp4lLhII= -gorm.io/plugin/dbresolver v1.5.2/go.mod h1:jPh59GOQbO7v7v28ZKZPd45tr+u3vyT+8tHdfdfOWcU= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= -gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= -modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= +modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= +modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/app/global.go b/internal/app/global.go new file mode 100644 index 00000000..62ece90c --- /dev/null +++ b/internal/app/global.go @@ -0,0 +1,29 @@ +package app + +import ( + "github.com/go-chi/chi/v5" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + "github.com/go-rat/sessions" + "github.com/knadh/koanf/v2" + "gorm.io/gorm" + + "github.com/TheTNB/panel/pkg/queue" +) + +var ( + Conf *koanf.Koanf + Http *chi.Mux + Orm *gorm.DB + Validator *validator.Validate + Translator *ut.Translator + Session *sessions.Manager + Queue *queue.Queue +) + +// 面板全局变量 +var ( + Root string + Version string + Locale string +) diff --git a/internal/backup.go b/internal/backup.go deleted file mode 100644 index 1f55f94d..00000000 --- a/internal/backup.go +++ /dev/null @@ -1,18 +0,0 @@ -package internal - -import ( - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Backup interface { - WebsiteList() ([]types.BackupFile, error) - WebSiteBackup(website models.Website) error - WebsiteRestore(website models.Website, backupFile string) error - MysqlList() ([]types.BackupFile, error) - MysqlBackup(database string) error - MysqlRestore(database string, backupFile string) error - PostgresqlList() ([]types.BackupFile, error) - PostgresqlBackup(database string) error - PostgresqlRestore(database string, backupFile string) error -} diff --git a/internal/biz/cert.go b/internal/biz/cert.go new file mode 100644 index 00000000..664a85f5 --- /dev/null +++ b/internal/biz/cert.go @@ -0,0 +1,40 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" +) + +type Cert struct { + ID uint `gorm:"primaryKey" json:"id"` + AccountID uint `gorm:"not null" json:"account_id"` // 关联的 ACME 账户 ID + WebsiteID uint `gorm:"not null" json:"website_id"` // 关联的网站 ID + DNSID uint `gorm:"not null" json:"dns_id"` // 关联的 DNS ID + Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 4096) + Domains []string `gorm:"not null;serializer:json" json:"domains"` + AutoRenew bool `gorm:"not null" json:"auto_renew"` // 自动续签 + CertURL string `gorm:"not null" json:"cert_url"` // 证书 URL (续签时使用) + Cert string `gorm:"not null" json:"cert"` // 证书内容 + Key string `gorm:"not null" json:"key"` // 私钥内容 + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + + Website *Website `gorm:"foreignKey:WebsiteID" json:"website"` + Account *CertAccount `gorm:"foreignKey:AccountID" json:"account"` + DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"` +} + +type CertRepo interface { + List(page, limit uint) ([]*Cert, int64, error) + Get(id uint) (*Cert, error) + Create(req *request.CertCreate) (*Cert, error) + Update(req *request.CertUpdate) error + Delete(id uint) error + ObtainAuto(id uint) (*acme.Certificate, error) + ObtainManual(id uint) (*acme.Certificate, error) + Renew(id uint) (*acme.Certificate, error) + ManualDNS(id uint) ([]acme.DNSRecord, error) + Deploy(ID, WebsiteID uint) error +} diff --git a/internal/biz/cert_account.go b/internal/biz/cert_account.go new file mode 100644 index 00000000..80482ee3 --- /dev/null +++ b/internal/biz/cert_account.go @@ -0,0 +1,29 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" +) + +type CertAccount struct { + ID uint `gorm:"primaryKey" json:"id"` + Email string `gorm:"not null" json:"email"` + CA string `gorm:"not null" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass) + Kid string `gorm:"not null" json:"kid"` + HmacEncoded string `gorm:"not null" json:"hmac_encoded"` + PrivateKey string `gorm:"not null" json:"private_key"` + KeyType string `gorm:"not null" json:"key_type"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + + Certs []*Cert `gorm:"foreignKey:AccountID" json:"-"` +} + +type CertAccountRepo interface { + List(page, limit uint) ([]*CertAccount, int64, error) + Get(id uint) (*CertAccount, error) + Create(req *request.CertAccountCreate) (*CertAccount, error) + Update(req *request.CertAccountUpdate) error + Delete(id uint) error +} diff --git a/internal/biz/cert_dns.go b/internal/biz/cert_dns.go new file mode 100644 index 00000000..df84a737 --- /dev/null +++ b/internal/biz/cert_dns.go @@ -0,0 +1,27 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" +) + +type CertDNS struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null" json:"name"` // 备注名称 + Type string `gorm:"not null" json:"type"` // DNS 提供商 (dnspod, tencent, aliyun, cloudflare) + Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + + Certs []*Cert `gorm:"foreignKey:DNSID" json:"-"` +} + +type CertDNSRepo interface { + List(page, limit uint) ([]*CertDNS, int64, error) + Get(id uint) (*CertDNS, error) + Create(req *request.CertDNSCreate) (*CertDNS, error) + Update(req *request.CertDNSUpdate) error + Delete(id uint) error +} diff --git a/internal/biz/container.go b/internal/biz/container.go new file mode 100644 index 00000000..754d0246 --- /dev/null +++ b/internal/biz/container.go @@ -0,0 +1,28 @@ +package biz + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerRepo interface { + ListAll() ([]types.Container, error) + ListByNames(names []string) ([]types.Container, error) + Create(req *request.ContainerCreate) (string, error) + Remove(id string) error + Start(id string) error + Stop(id string) error + Restart(id string) error + Pause(id string) error + Unpause(id string) error + Inspect(id string) (types.ContainerJSON, error) + Kill(id string) error + Rename(id string, newName string) error + Stats(id string) (container.StatsResponseReader, error) + Exist(name string) (bool, error) + Update(id string, config container.UpdateConfig) error + Logs(id string) (string, error) + Prune() error +} diff --git a/internal/biz/container_image.go b/internal/biz/container_image.go new file mode 100644 index 00000000..555f000d --- /dev/null +++ b/internal/biz/container_image.go @@ -0,0 +1,17 @@ +package biz + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/image" + + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerImageRepo interface { + List() ([]image.Summary, error) + Exist(id string) (bool, error) + Pull(req *request.ContainerImagePull) error + Remove(id string) error + Prune() error + Inspect(id string) (types.ImageInspect, error) +} diff --git a/internal/biz/container_network.go b/internal/biz/container_network.go new file mode 100644 index 00000000..48499515 --- /dev/null +++ b/internal/biz/container_network.go @@ -0,0 +1,18 @@ +package biz + +import ( + "github.com/docker/docker/api/types/network" + + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerNetworkRepo interface { + List() ([]network.Inspect, error) + Create(req *request.ContainerNetworkCreate) (string, error) + Remove(id string) error + Exist(name string) (bool, error) + Inspect(id string) (network.Inspect, error) + Connect(networkID string, containerID string) error + Disconnect(networkID string, containerID string) error + Prune() error +} diff --git a/internal/biz/container_volume.go b/internal/biz/container_volume.go new file mode 100644 index 00000000..3e1659cb --- /dev/null +++ b/internal/biz/container_volume.go @@ -0,0 +1,16 @@ +package biz + +import ( + "github.com/docker/docker/api/types/volume" + + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerVolumeRepo interface { + List() ([]*volume.Volume, error) + Create(req *request.ContainerVolumeCreate) (volume.Volume, error) + Exist(name string) (bool, error) + Inspect(id string) (volume.Volume, error) + Remove(id string) error + Prune() error +} diff --git a/internal/biz/cron.go b/internal/biz/cron.go new file mode 100644 index 00000000..46b982f1 --- /dev/null +++ b/internal/biz/cron.go @@ -0,0 +1,30 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" +) + +type Cron struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;unique" json:"name"` + Status bool `gorm:"not null" json:"status"` + Type string `gorm:"not null" json:"type"` + Time string `gorm:"not null" json:"time"` + Shell string `gorm:"not null" json:"shell"` + Log string `gorm:"not null" json:"log"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type CronRepo interface { + Count() (int64, error) + List(page, limit uint) ([]*Cron, int64, error) + Get(id uint) (*Cron, error) + Create(req *request.CronCreate) error + Update(req *request.CronUpdate) error + Delete(id uint) error + Status(id uint, status bool) error + Log(id uint) (string, error) +} diff --git a/internal/biz/database.go b/internal/biz/database.go new file mode 100644 index 00000000..f43ac4ab --- /dev/null +++ b/internal/biz/database.go @@ -0,0 +1,16 @@ +package biz + +import "github.com/golang-module/carbon/v2" + +type Database struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;unique" json:"name"` + Type string `gorm:"not null" json:"type"` + Host string `gorm:"not null" json:"host"` + Port int `gorm:"not null" json:"port"` + Username string `gorm:"not null" json:"username"` + Password string `gorm:"not null" json:"password"` + Remark string `gorm:"not null" json:"remark"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} diff --git a/internal/biz/firewall.go b/internal/biz/firewall.go new file mode 100644 index 00000000..73ea74f8 --- /dev/null +++ b/internal/biz/firewall.go @@ -0,0 +1,4 @@ +package biz + +type FirewallRepo interface { +} diff --git a/internal/biz/monitor.go b/internal/biz/monitor.go new file mode 100644 index 00000000..cc60ac6f --- /dev/null +++ b/internal/biz/monitor.go @@ -0,0 +1,22 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/tools" +) + +type Monitor struct { + ID uint `gorm:"primaryKey" json:"id"` + Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type MonitorRepo interface { + GetSetting() (*request.MonitorSetting, error) + UpdateSetting(setting *request.MonitorSetting) error + Clear() error + List(start, end carbon.Carbon) ([]*Monitor, error) +} diff --git a/internal/biz/plugin.go b/internal/biz/plugin.go new file mode 100644 index 00000000..21a5a65a --- /dev/null +++ b/internal/biz/plugin.go @@ -0,0 +1,30 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/pkg/types" +) + +type Plugin struct { + ID uint `gorm:"primaryKey" json:"id"` + Slug string `gorm:"not null;unique" json:"slug"` + Version string `gorm:"not null" json:"version"` + Show bool `gorm:"not null" json:"show"` + ShowOrder int `gorm:"not null" json:"show_order"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type PluginRepo interface { + All() []*types.Plugin + Installed() ([]*Plugin, error) + Get(slug string) (*types.Plugin, error) + GetInstalled(slug string) (*Plugin, error) + GetInstalledAll(cond ...string) ([]*Plugin, error) + IsInstalled(cond ...string) (bool, error) + Install(slug string) error + Uninstall(slug string) error + Update(slug string) error + UpdateShow(slug string, show bool) error +} diff --git a/internal/biz/safe.go b/internal/biz/safe.go new file mode 100644 index 00000000..f9ba6fe5 --- /dev/null +++ b/internal/biz/safe.go @@ -0,0 +1,8 @@ +package biz + +type SafeRepo interface { + GetSSH() (uint, bool, error) + UpdateSSH(port uint, status bool) error + GetPingStatus() (bool, error) + UpdatePingStatus(status bool) error +} diff --git a/internal/biz/setting.go b/internal/biz/setting.go new file mode 100644 index 00000000..d09f3c3c --- /dev/null +++ b/internal/biz/setting.go @@ -0,0 +1,39 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" +) + +type SettingKey string + +const ( + SettingKeyName SettingKey = "name" + SettingKeyVersion SettingKey = "version" + SettingKeyMonitor SettingKey = "monitor" + SettingKeyMonitorDays SettingKey = "monitor_days" + SettingKeyBackupPath SettingKey = "backup_path" + SettingKeyWebsitePath SettingKey = "website_path" + SettingKeyMysqlRootPassword SettingKey = "mysql_root_password" + SettingKeySshHost SettingKey = "ssh_host" + SettingKeySshPort SettingKey = "ssh_port" + SettingKeySshUser SettingKey = "ssh_user" + SettingKeySshPassword SettingKey = "ssh_password" +) + +type Setting struct { + ID uint `gorm:"primaryKey" json:"id"` + Key SettingKey `gorm:"not null;unique" json:"key"` + Value string `gorm:"not null" json:"value"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type SettingRepo interface { + Get(key SettingKey, defaultValue ...string) (string, error) + Set(key SettingKey, value string) error + Delete(key SettingKey) error + GetPanelSetting() (*request.PanelSetting, error) + UpdatePanelSetting(setting *request.PanelSetting) error +} diff --git a/internal/biz/ssh.go b/internal/biz/ssh.go new file mode 100644 index 00000000..bfa4664d --- /dev/null +++ b/internal/biz/ssh.go @@ -0,0 +1,8 @@ +package biz + +import "github.com/TheTNB/panel/internal/http/request" + +type SSHRepo interface { + GetInfo() (map[string]any, error) + UpdateInfo(req *request.SSHUpdateInfo) error +} diff --git a/internal/biz/task.go b/internal/biz/task.go new file mode 100644 index 00000000..633c72e9 --- /dev/null +++ b/internal/biz/task.go @@ -0,0 +1,30 @@ +package biz + +import "github.com/golang-module/carbon/v2" + +type TaskStatus string + +const ( + TaskStatusWaiting TaskStatus = "waiting" + TaskStatusRunning TaskStatus = "running" + TaskStatusSuccess TaskStatus = "finished" + TaskStatusFailed TaskStatus = "failed" +) + +type Task struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;index" json:"name"` + Status TaskStatus `gorm:"not null;default:'waiting'" json:"status"` + Shell string `gorm:"not null" json:"-"` + Log string `gorm:"not null" json:"log"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` +} + +type TaskRepo interface { + HasRunningTask() bool + List(page, limit uint) ([]*Task, int64, error) + Get(id uint) (*Task, error) + Delete(id uint) error + UpdateStatus(id uint, status TaskStatus) error +} diff --git a/internal/biz/user.go b/internal/biz/user.go new file mode 100644 index 00000000..a6fad865 --- /dev/null +++ b/internal/biz/user.go @@ -0,0 +1,22 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + "gorm.io/gorm" +) + +type User struct { + ID uint `gorm:"primaryKey" json:"id"` + Username string `gorm:"not null;unique" json:"username"` + Password string `gorm:"not null" json:"password"` + Email string `gorm:"not null" json:"email"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` +} + +type UserRepo interface { + CheckPassword(username, password string) (*User, error) + Get(id uint) (*User, error) + Save(user *User) error +} diff --git a/internal/biz/website.go b/internal/biz/website.go new file mode 100644 index 00000000..1191c290 --- /dev/null +++ b/internal/biz/website.go @@ -0,0 +1,36 @@ +package biz + +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/types" +) + +type Website struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"not null;unique" json:"name"` + Status bool `gorm:"not null;default:true" json:"status"` + Path string `gorm:"not null" json:"path"` + PHP int `gorm:"not null" json:"php"` + SSL bool `gorm:"not null" json:"ssl"` + Remark string `gorm:"not null" json:"remark"` + CreatedAt carbon.DateTime `json:"created_at"` + UpdatedAt carbon.DateTime `json:"updated_at"` + + Cert *Cert `gorm:"foreignKey:WebsiteID" json:"cert"` +} + +type WebsiteRepo interface { + UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error + Count() (int64, error) + Get(id uint) (*types.WebsiteSetting, error) + List(page, limit uint) ([]*Website, int64, error) + Create(req *request.WebsiteCreate) (*Website, error) + Update(req *request.WebsiteUpdate) error + Delete(req *request.WebsiteDelete) error + ClearLog(id uint) error + UpdateRemark(id uint, remark string) error + ResetConfig(id uint) error + UpdateStatus(id uint, status bool) error +} diff --git a/internal/bootstrap/app.go b/internal/bootstrap/app.go new file mode 100644 index 00000000..9669966c --- /dev/null +++ b/internal/bootstrap/app.go @@ -0,0 +1,21 @@ +package bootstrap + +import ( + "runtime/debug" +) + +func Boot() { + debug.SetGCPercent(10) + debug.SetMemoryLimit(64 << 20) + + initConf() + initGlobal() + initOrm() + runMigrate() + initValidator() + initSession() + initQueue() + go initHttp() + + select {} +} diff --git a/internal/bootstrap/conf.go b/internal/bootstrap/conf.go new file mode 100644 index 00000000..2a5b7d96 --- /dev/null +++ b/internal/bootstrap/conf.go @@ -0,0 +1,24 @@ +package bootstrap + +import ( + "fmt" + + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + + "github.com/TheTNB/panel/internal/app" +) + +func initConf() { + app.Conf = koanf.New(".") + if err := app.Conf.Load(file.Provider("config/config.yml"), yaml.Parser()); err != nil { + panic(fmt.Sprintf("failed to load config: %v", err)) + } +} + +func initGlobal() { + app.Root = app.Conf.MustString("app.root") + app.Version = app.Conf.MustString("app.version") + app.Locale = app.Conf.MustString("app.locale") +} diff --git a/internal/bootstrap/db.go b/internal/bootstrap/db.go new file mode 100644 index 00000000..9038afda --- /dev/null +++ b/internal/bootstrap/db.go @@ -0,0 +1,40 @@ +package bootstrap + +import ( + "fmt" + + "github.com/glebarez/sqlite" + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/migration" +) + +func initOrm() { + logLevel := logger.Error + if app.Conf.Bool("database.debug") { + logLevel = logger.Info + } + // You can use any other database, like MySQL or PostgreSQL. + db, err := gorm.Open(sqlite.Open("storage/panel.db"), &gorm.Config{ + Logger: logger.Default.LogMode(logLevel), + SkipDefaultTransaction: true, + DisableForeignKeyConstraintWhenMigrating: true, + }) + if err != nil { + panic(fmt.Sprintf("failed to connect database: %v", err)) + } + app.Orm = db +} + +func runMigrate() { + migrator := gormigrate.New(app.Orm, &gormigrate.Options{ + UseTransaction: true, // Note: MySQL not support DDL transaction + ValidateUnknownMigrations: true, + }, migration.Migrations) + if err := migrator.Migrate(); err != nil { + panic(fmt.Sprintf("failed to migrate database: %v", err)) + } +} diff --git a/internal/bootstrap/http.go b/internal/bootstrap/http.go new file mode 100644 index 00000000..5539b480 --- /dev/null +++ b/internal/bootstrap/http.go @@ -0,0 +1,33 @@ +package bootstrap + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/http/middleware" + "github.com/TheTNB/panel/internal/plugin" + "github.com/TheTNB/panel/internal/route" +) + +func initHttp() { + app.Http = chi.NewRouter() + + // add middleware + app.Http.Use(middleware.GlobalMiddleware()...) + + // add route + route.Http(app.Http) + plugin.Boot(app.Http) + + server := &http.Server{ + Addr: app.Conf.MustString("http.address"), + Handler: http.AllowQuerySemicolons(app.Http), + MaxHeaderBytes: 2048 << 20, + } + if err := server.ListenAndServe(); err != nil { + panic(fmt.Sprintf("failed to start http server: %v", err)) + } +} diff --git a/internal/bootstrap/queue.go b/internal/bootstrap/queue.go new file mode 100644 index 00000000..69fec45f --- /dev/null +++ b/internal/bootstrap/queue.go @@ -0,0 +1,11 @@ +package bootstrap + +import ( + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/pkg/queue" +) + +func initQueue() { + app.Queue = queue.New() + go app.Queue.Run() +} diff --git a/internal/bootstrap/session.go b/internal/bootstrap/session.go new file mode 100644 index 00000000..aeeb8416 --- /dev/null +++ b/internal/bootstrap/session.go @@ -0,0 +1,31 @@ +package bootstrap + +import ( + "fmt" + + "github.com/go-rat/gormstore" + "github.com/go-rat/sessions" + + "github.com/TheTNB/panel/internal/app" +) + +func initSession() { + // initialize session manager + manager, err := sessions.NewManager(&sessions.ManagerOptions{ + Key: app.Conf.String("app.key"), + Lifetime: 120, + GcInterval: 30, + DisableDefaultDriver: true, + }) + if err != nil { + panic(fmt.Sprintf("failed to initialize session manager: %v", err)) + } + + // extend gorm store driver + store := gormstore.New(app.Orm) + if err = manager.Extend("default", store); err != nil { + panic(fmt.Sprintf("failed to extend session manager: %v", err)) + } + + app.Session = manager +} diff --git a/internal/bootstrap/validator.go b/internal/bootstrap/validator.go new file mode 100644 index 00000000..2427fb1c --- /dev/null +++ b/internal/bootstrap/validator.go @@ -0,0 +1,26 @@ +package bootstrap + +import ( + "fmt" + + "github.com/go-playground/locales/zh_Hans_CN" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + "github.com/go-playground/validator/v10/translations/zh" + + "github.com/TheTNB/panel/internal/app" +) + +func initValidator() { + translator := zh_Hans_CN.New() + uni := ut.New(translator, translator) + trans, _ := uni.GetTranslator("zh_Hans_CN") + + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := zh.RegisterDefaultTranslations(validate, trans); err != nil { + panic(fmt.Sprintf("failed to register validator translations: %v", err)) + } + + app.Translator = &trans + app.Validator = validate +} diff --git a/internal/cert.go b/internal/cert.go deleted file mode 100644 index 98e7dcaa..00000000 --- a/internal/cert.go +++ /dev/null @@ -1,27 +0,0 @@ -package internal - -import ( - requests "github.com/TheTNB/panel/v2/app/http/requests/cert" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/acme" -) - -type Cert interface { - UserStore(request requests.UserStore) error - UserUpdate(request requests.UserUpdate) error - UserShow(ID uint) (models.CertUser, error) - UserDestroy(ID uint) error - DNSStore(request requests.DNSStore) error - DNSUpdate(request requests.DNSUpdate) error - DNSShow(ID uint) (models.CertDNS, error) - DNSDestroy(ID uint) error - CertStore(request requests.CertStore) error - CertUpdate(request requests.CertUpdate) error - CertShow(ID uint) (models.Cert, error) - CertDestroy(ID uint) error - ObtainAuto(ID uint) (acme.Certificate, error) - ObtainManual(ID uint) (acme.Certificate, error) - ManualDNS(ID uint) ([]acme.DNSRecord, error) - Renew(ID uint) (acme.Certificate, error) - Deploy(ID, WebsiteID uint) error -} diff --git a/internal/container.go b/internal/container.go deleted file mode 100644 index 29415e67..00000000 --- a/internal/container.go +++ /dev/null @@ -1,54 +0,0 @@ -package internal - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/volume" - - requests "github.com/TheTNB/panel/v2/app/http/requests/container" - paneltypes "github.com/TheTNB/panel/v2/pkg/types" -) - -type Container interface { - ContainerListAll() ([]types.Container, error) - ContainerListByNames(names []string) ([]types.Container, error) - ContainerCreate(name string, config container.Config, host container.HostConfig, networkConfig network.NetworkingConfig) (string, error) - ContainerRemove(id string) error - ContainerStart(id string) error - ContainerStop(id string) error - ContainerRestart(id string) error - ContainerPause(id string) error - ContainerUnpause(id string) error - ContainerInspect(id string) (types.ContainerJSON, error) - ContainerKill(id string) error - ContainerRename(id string, newName string) error - ContainerStats(id string) (container.StatsResponseReader, error) - ContainerExist(name string) (bool, error) - ContainerUpdate(id string, config container.UpdateConfig) error - ContainerLogs(id string) (string, error) - ContainerPrune() error - NetworkList() ([]network.Inspect, error) - NetworkCreate(config requests.NetworkCreate) (string, error) - NetworkRemove(id string) error - NetworkExist(name string) (bool, error) - NetworkInspect(id string) (network.Inspect, error) - NetworkConnect(networkID string, containerID string) error - NetworkDisconnect(networkID string, containerID string) error - NetworkPrune() error - ImageList() ([]image.Summary, error) - ImageExist(id string) (bool, error) - ImagePull(config requests.ImagePull) error - ImageRemove(id string) error - ImagePrune() error - ImageInspect(id string) (types.ImageInspect, error) - VolumeList() ([]*volume.Volume, error) - VolumeCreate(config requests.VolumeCreate) (volume.Volume, error) - VolumeExist(name string) (bool, error) - VolumeInspect(id string) (volume.Volume, error) - VolumeRemove(id string) error - VolumePrune() error - KVToMap(kvs []paneltypes.KV) map[string]string - KVToSlice(kvs []paneltypes.KV) []string -} diff --git a/internal/cron.go b/internal/cron.go deleted file mode 100644 index 91b792bf..00000000 --- a/internal/cron.go +++ /dev/null @@ -1,8 +0,0 @@ -package internal - -import "github.com/TheTNB/panel/v2/app/models" - -type Cron interface { - AddToSystem(cron models.Cron) error - DeleteFromSystem(cron models.Cron) error -} diff --git a/internal/data/cert.go b/internal/data/cert.go new file mode 100644 index 00000000..208f311c --- /dev/null +++ b/internal/data/cert.go @@ -0,0 +1,273 @@ +package data + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type certRepo struct { + client *acme.Client + websiteRepo biz.WebsiteRepo +} + +func NewCertRepo() biz.CertRepo { + return &certRepo{ + websiteRepo: NewWebsiteRepo(), + } +} + +func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) { + var certs []*biz.Cert + var total int64 + err := app.Orm.Model(&biz.Cert{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error + return certs, total, err +} + +func (r *certRepo) Get(id uint) (*biz.Cert, error) { + cert := new(biz.Cert) + err := app.Orm.Model(&biz.Cert{}).Where("id = ?", id).First(cert).Error + return cert, err +} + +func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) { + cert := &biz.Cert{ + AccountID: req.AccountID, + WebsiteID: req.WebsiteID, + DNSID: req.DNSID, + Type: req.Type, + Domains: req.Domains, + AutoRenew: req.AutoRenew, + } + if err := app.Orm.Create(cert).Error; err != nil { + return nil, err + } + return cert, nil +} + +func (r *certRepo) Update(req *request.CertUpdate) error { + return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Updates(&biz.Cert{ + AccountID: req.AccountID, + WebsiteID: req.WebsiteID, + DNSID: req.DNSID, + Type: req.Type, + Domains: req.Domains, + AutoRenew: req.AutoRenew, + }).Error +} + +func (r *certRepo) Delete(id uint) error { + return app.Orm.Model(&biz.Cert{}).Where("id = ?", id).Delete(&biz.Cert{}).Error +} + +func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) { + cert, err := r.Get(id) + if err != nil { + return nil, err + } + + client, err := r.getClient(cert) + if err != nil { + return nil, err + } + + if cert.DNS != nil { + client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) + } else { + if cert.Website == nil { + return nil, errors.New("该证书没有关联网站,无法自动签发") + } else { + for _, domain := range cert.Domains { + if strings.Contains(domain, "*") { + return nil, errors.New("通配符域名无法使用 HTTP 验证") + } + } + conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name) + client.UseHTTP(conf, cert.Website.Path) + } + } + + ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type)) + if err != nil { + return nil, err + } + + cert.CertURL = ssl.URL + cert.Cert = string(ssl.ChainPEM) + cert.Key = string(ssl.PrivateKey) + if err = app.Orm.Save(cert).Error; err != nil { + return nil, err + } + + if cert.Website != nil { + return &ssl, r.Deploy(cert.ID, cert.WebsiteID) + } + + return &ssl, nil +} + +func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) { + cert, err := r.Get(id) + if err != nil { + return nil, err + } + + if r.client == nil { + return nil, errors.New("请重新获取 DNS 解析记录") + } + + ssl, err := r.client.ObtainSSLManual() + if err != nil { + return nil, err + } + + cert.CertURL = ssl.URL + cert.Cert = string(ssl.ChainPEM) + cert.Key = string(ssl.PrivateKey) + if err = app.Orm.Save(cert).Error; err != nil { + return nil, err + } + + if cert.Website != nil { + return &ssl, r.Deploy(cert.ID, cert.WebsiteID) + } + + return &ssl, nil +} + +func (r *certRepo) Renew(id uint) (*acme.Certificate, error) { + cert, err := r.Get(id) + if err != nil { + return nil, err + } + + client, err := r.getClient(cert) + if err != nil { + return nil, err + } + + if cert.CertURL == "" { + return nil, errors.New("该证书没有签发成功,无法续签") + } + + if cert.DNS != nil { + client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) + } else { + if cert.Website == nil { + return nil, errors.New("该证书没有关联网站,无法续签,可以尝试手动签发") + } else { + for _, domain := range cert.Domains { + if strings.Contains(domain, "*") { + return nil, errors.New("通配符域名无法使用 HTTP 验证") + } + } + conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name) + client.UseHTTP(conf, cert.Website.Path) + } + } + + ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type)) + if err != nil { + return nil, err + } + + cert.CertURL = ssl.URL + cert.Cert = string(ssl.ChainPEM) + cert.Key = string(ssl.PrivateKey) + if err = app.Orm.Save(cert).Error; err != nil { + return nil, err + } + + if cert.Website != nil { + return &ssl, r.Deploy(cert.ID, cert.WebsiteID) + } + + return &ssl, nil +} + +func (r *certRepo) ManualDNS(id uint) ([]acme.DNSRecord, error) { + cert, err := r.Get(id) + if err != nil { + return nil, err + } + + client, err := r.getClient(cert) + if err != nil { + return nil, err + } + + client.UseManualDns(len(cert.Domains)) + records, err := client.GetDNSRecords(context.Background(), cert.Domains, acme.KeyType(cert.Type)) + if err != nil { + return nil, err + } + + // 15 分钟后清理客户端 + r.client = client + time.AfterFunc(15*time.Minute, func() { + r.client = nil + }) + + return records, nil +} + +func (r *certRepo) Deploy(ID, WebsiteID uint) error { + cert, err := r.Get(ID) + if err != nil { + return err + } + + if cert.Cert == "" || cert.Key == "" { + return errors.New("该证书没有签发成功,无法部署") + } + + website, err := r.websiteRepo.Get(WebsiteID) + if err != nil { + return err + } + + if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil { + return err + } + if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil { + return err + } + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return err + } + + return nil +} + +func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) { + var ca string + var eab *acme.EAB + switch cert.Account.CA { + case "letsencrypt": + ca = acme.CALetsEncrypt + case "buypass": + ca = acme.CABuypass + case "zerossl": + ca = acme.CAZeroSSL + eab = &acme.EAB{KeyID: cert.Account.Kid, MACKey: cert.Account.HmacEncoded} + case "sslcom": + ca = acme.CASSLcom + eab = &acme.EAB{KeyID: cert.Account.Kid, MACKey: cert.Account.HmacEncoded} + case "google": + ca = acme.CAGoogle + eab = &acme.EAB{KeyID: cert.Account.Kid, MACKey: cert.Account.HmacEncoded} + } + + return acme.NewPrivateKeyAccount(cert.Account.Email, cert.Account.PrivateKey, ca, eab) +} diff --git a/internal/data/cert_account.go b/internal/data/cert_account.go new file mode 100644 index 00000000..d43e5e0b --- /dev/null +++ b/internal/data/cert_account.go @@ -0,0 +1,154 @@ +package data + +import ( + "context" + "errors" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" + "github.com/TheTNB/panel/pkg/cert" +) + +type certAccountRepo struct{} + +func NewCertAccountRepo() biz.CertAccountRepo { + return &certAccountRepo{} +} + +func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, error) { + var accounts []*biz.CertAccount + var total int64 + err := app.Orm.Model(&biz.CertAccount{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&accounts).Error + return accounts, total, err +} + +func (r certAccountRepo) Get(id uint) (*biz.CertAccount, error) { + account := new(biz.CertAccount) + err := app.Orm.Model(&biz.CertAccount{}).Where("id = ?", id).First(account).Error + return account, err +} + +func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccount, error) { + account := new(biz.CertAccount) + account.CA = req.CA + account.Email = req.Email + account.Kid = req.Kid + account.HmacEncoded = req.HmacEncoded + account.KeyType = req.KeyType + + var err error + var client *acme.Client + switch account.CA { + case "letsencrypt": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType)) + case "buypass": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType)) + case "zerossl": + eab, eabErr := r.getZeroSSLEAB(account.Email) + if eabErr != nil { + return nil, eabErr + } + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType)) + case "sslcom": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType)) + case "google": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType)) + default: + return nil, errors.New("CA 提供商不支持") + } + + if err != nil { + return nil, errors.New("向 CA 注册账号失败,请检查参数是否正确") + } + + privateKey, err := cert.EncodeKey(client.Account.PrivateKey) + if err != nil { + return nil, errors.New("获取私钥失败") + } + account.PrivateKey = string(privateKey) + + if err = app.Orm.Create(account).Error; err != nil { + return nil, err + } + + return account, nil +} + +func (r certAccountRepo) Update(req *request.CertAccountUpdate) error { + account, err := r.Get(req.ID) + if err != nil { + return err + } + + account.CA = req.CA + account.Email = req.Email + account.Kid = req.Kid + account.HmacEncoded = req.HmacEncoded + account.KeyType = req.KeyType + + var client *acme.Client + switch account.CA { + case "letsencrypt": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType)) + case "buypass": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType)) + case "zerossl": + eab, eabErr := r.getZeroSSLEAB(account.Email) + if eabErr != nil { + return eabErr + } + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType)) + case "sslcom": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType)) + case "google": + client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType)) + default: + return errors.New("CA 提供商不支持") + } + + if err != nil { + return errors.New("向 CA 注册账号失败,请检查参数是否正确") + } + + privateKey, err := cert.EncodeKey(client.Account.PrivateKey) + if err != nil { + return errors.New("获取私钥失败") + } + account.PrivateKey = string(privateKey) + + return app.Orm.Save(account).Error +} + +func (r certAccountRepo) Delete(id uint) error { + return app.Orm.Model(&biz.CertAccount{}).Where("id = ?", id).Delete(&biz.CertAccount{}).Error +} + +// getZeroSSLEAB 获取 ZeroSSL EAB +func (r certAccountRepo) getZeroSSLEAB(email string) (*acme.EAB, error) { + type data struct { + Success bool `json:"success"` + EabKid string `json:"eab_kid"` + EabHmacKey string `json:"eab_hmac_key"` + } + client := resty.New() + client.SetTimeout(5 * time.Second) + client.SetRetryCount(2) + + resp, err := client.R().SetFormData(map[string]string{ + "email": email, + }).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email") + if err != nil || !resp.IsSuccess() { + return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败") + } + eab := resp.Result().(*data) + if !eab.Success { + return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败") + } + + return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil +} diff --git a/internal/data/cert_dns.go b/internal/data/cert_dns.go new file mode 100644 index 00000000..a0c2158e --- /dev/null +++ b/internal/data/cert_dns.go @@ -0,0 +1,57 @@ +package data + +import ( + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type certDNSRepo struct{} + +func NewCertDNSRepo() biz.CertDNSRepo { + return &certDNSRepo{} +} + +func (r certDNSRepo) List(page, limit uint) ([]*biz.CertDNS, int64, error) { + var certDNS []*biz.CertDNS + var total int64 + err := app.Orm.Model(&biz.CertDNS{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certDNS).Error + return certDNS, total, err +} + +func (r certDNSRepo) Get(id uint) (*biz.CertDNS, error) { + certDNS := new(biz.CertDNS) + err := app.Orm.Model(&biz.CertDNS{}).Where("id = ?", id).First(certDNS).Error + return certDNS, err +} + +func (r certDNSRepo) Create(req *request.CertDNSCreate) (*biz.CertDNS, error) { + certDNS := &biz.CertDNS{ + Name: req.Name, + Type: req.Type, + Data: req.Data, + } + + if err := app.Orm.Create(certDNS).Error; err != nil { + return nil, err + } + + return certDNS, nil +} + +func (r certDNSRepo) Update(req *request.CertDNSUpdate) error { + cert, err := r.Get(req.ID) + if err != nil { + return err + } + + cert.Name = req.Name + cert.Type = req.Type + cert.Data = req.Data + + return app.Orm.Save(cert).Error +} + +func (r certDNSRepo) Delete(id uint) error { + return app.Orm.Model(&biz.CertDNS{}).Where("id = ?", id).Delete(&biz.CertDNS{}).Error +} diff --git a/internal/data/container.go b/internal/data/container.go new file mode 100644 index 00000000..700c0d93 --- /dev/null +++ b/internal/data/container.go @@ -0,0 +1,235 @@ +package data + +import ( + "context" + "fmt" + "io" + "strconv" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + paneltypes "github.com/TheTNB/panel/pkg/types" +) + +type containerRepo struct { + client *client.Client +} + +func NewContainerRepo(sock ...string) biz.ContainerRepo { + if len(sock) == 0 { + sock = append(sock, "/run/podman/podman.sock") + } + cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) + return &containerRepo{ + client: cli, + } +} + +// ListAll 列出所有容器 +func (r *containerRepo) ListAll() ([]types.Container, error) { + containers, err := r.client.ContainerList(context.Background(), container.ListOptions{ + All: true, + }) + if err != nil { + return nil, err + } + + return containers, nil +} + +// ListByNames 根据名称列出容器 +func (r *containerRepo) ListByNames(names []string) ([]types.Container, error) { + var options container.ListOptions + options.All = true + if len(names) > 0 { + var array []filters.KeyValuePair + for _, n := range names { + array = append(array, filters.Arg("name", n)) + } + options.Filters = filters.NewArgs(array...) + } + containers, err := r.client.ContainerList(context.Background(), options) + if err != nil { + return nil, err + } + + return containers, nil +} + +// Create 创建容器 +func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { + var hostConf container.HostConfig + var networkConf network.NetworkingConfig + + portMap := make(nat.PortMap) + for _, port := range req.Ports { + if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd { + return "", fmt.Errorf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd) + } + if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 { + return "", fmt.Errorf("端口范围不正确") + } + + count := 0 + for host := port.HostStart; host <= port.HostEnd; host++ { + bindItem := nat.PortBinding{HostPort: strconv.Itoa(host), HostIP: port.Host} + portMap[nat.Port(fmt.Sprintf("%d/%s", port.ContainerStart+count, port.Protocol))] = []nat.PortBinding{bindItem} + count++ + } + } + + exposed := make(nat.PortSet) + for port := range portMap { + exposed[port] = struct{}{} + } + + if req.Network != "" { + switch req.Network { + case "host", "none", "bridge": + hostConf.NetworkMode = container.NetworkMode(req.Network) + } + networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}} + } else { + networkConf = network.NetworkingConfig{} + } + + hostConf.Privileged = req.Privileged + hostConf.AutoRemove = req.AutoRemove + hostConf.CPUShares = req.CPUShares + hostConf.PublishAllPorts = req.PublishAllPorts + hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)} + if req.RestartPolicy == "on-failure" { + hostConf.RestartPolicy.MaximumRetryCount = 5 + } + hostConf.NanoCPUs = req.CPUs * 1000000000 + hostConf.Memory = req.Memory * 1024 * 1024 + hostConf.MemorySwap = 0 + hostConf.PortBindings = portMap + hostConf.Binds = []string{} + + volumes := make(map[string]struct{}) + for _, v := range req.Volumes { + volumes[v.Container] = struct{}{} + hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", v.Host, v.Container, v.Mode)) + } + + resp, err := r.client.ContainerCreate(context.Background(), &container.Config{ + Image: req.Image, + Env: paneltypes.KVToSlice(req.Env), + Entrypoint: req.Entrypoint, + Cmd: req.Command, + Labels: paneltypes.KVToMap(req.Labels), + ExposedPorts: exposed, + OpenStdin: req.OpenStdin, + Tty: req.Tty, + Volumes: volumes, + }, &hostConf, &networkConf, nil, req.Name) + if err != nil { + return "", err + } + + return resp.ID, err +} + +// Remove 移除容器 +func (r *containerRepo) Remove(id string) error { + return r.client.ContainerRemove(context.Background(), id, container.RemoveOptions{ + Force: true, + }) +} + +// Start 启动容器 +func (r *containerRepo) Start(id string) error { + return r.client.ContainerStart(context.Background(), id, container.StartOptions{}) +} + +// Stop 停止容器 +func (r *containerRepo) Stop(id string) error { + return r.client.ContainerStop(context.Background(), id, container.StopOptions{}) +} + +// Restart 重启容器 +func (r *containerRepo) Restart(id string) error { + return r.client.ContainerRestart(context.Background(), id, container.StopOptions{}) +} + +// Pause 暂停容器 +func (r *containerRepo) Pause(id string) error { + return r.client.ContainerPause(context.Background(), id) +} + +// Unpause 恢复容器 +func (r *containerRepo) Unpause(id string) error { + return r.client.ContainerUnpause(context.Background(), id) +} + +// Inspect 查看容器 +func (r *containerRepo) Inspect(id string) (types.ContainerJSON, error) { + return r.client.ContainerInspect(context.Background(), id) +} + +// Kill 杀死容器 +func (r *containerRepo) Kill(id string) error { + return r.client.ContainerKill(context.Background(), id, "KILL") +} + +// Rename 重命名容器 +func (r *containerRepo) Rename(id string, newName string) error { + return r.client.ContainerRename(context.Background(), id, newName) +} + +// Stats 查看容器状态 +func (r *containerRepo) Stats(id string) (container.StatsResponseReader, error) { + return r.client.ContainerStats(context.Background(), id, false) +} + +// Exist 判断容器是否存在 +func (r *containerRepo) Exist(name string) (bool, error) { + var options container.ListOptions + options.Filters = filters.NewArgs(filters.Arg("name", name)) + containers, err := r.client.ContainerList(context.Background(), options) + if err != nil { + return false, err + } + + return len(containers) > 0, nil +} + +// Update 更新容器 +func (r *containerRepo) Update(id string, config container.UpdateConfig) error { + _, err := r.client.ContainerUpdate(context.Background(), id, config) + return err +} + +// Logs 查看容器日志 +func (r *containerRepo) Logs(id string) (string, error) { + options := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + } + reader, err := r.client.ContainerLogs(context.Background(), id, options) + if err != nil { + return "", err + } + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + return "", err + } + + return string(data), nil +} + +// Prune 清理未使用的容器 +func (r *containerRepo) Prune() error { + _, err := r.client.ContainersPrune(context.Background(), filters.NewArgs()) + return err +} diff --git a/internal/data/container_image.go b/internal/data/container_image.go new file mode 100644 index 00000000..a258d104 --- /dev/null +++ b/internal/data/container_image.go @@ -0,0 +1,97 @@ +package data + +import ( + "context" + "encoding/base64" + "encoding/json" + "io" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/client" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type containerImageRepo struct { + client *client.Client +} + +func NewContainerImageRepo(sock ...string) biz.ContainerImageRepo { + if len(sock) == 0 { + sock = append(sock, "/run/podman/podman.sock") + } + cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) + return &containerImageRepo{ + client: cli, + } +} + +// List 列出镜像 +func (r *containerImageRepo) List() ([]image.Summary, error) { + return r.client.ImageList(context.Background(), image.ListOptions{ + All: true, + }) +} + +// Exist 判断镜像是否存在 +func (r *containerImageRepo) Exist(id string) (bool, error) { + var options image.ListOptions + options.Filters = filters.NewArgs(filters.Arg("reference", id)) + images, err := r.client.ImageList(context.Background(), options) + if err != nil { + return false, err + } + + return len(images) > 0, nil +} + +// Pull 拉取镜像 +func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { + options := image.PullOptions{} + if req.Auth { + authConfig := registry.AuthConfig{ + Username: req.Username, + Password: req.Password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + return err + } + authStr := base64.URLEncoding.EncodeToString(encodedJSON) + options.RegistryAuth = authStr + } + + out, err := r.client.ImagePull(context.Background(), req.Name, options) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(io.Discard, out) + return err +} + +// Remove 删除镜像 +func (r *containerImageRepo) Remove(id string) error { + _, err := r.client.ImageRemove(context.Background(), id, image.RemoveOptions{ + Force: true, + PruneChildren: true, + }) + return err +} + +// Prune 清理未使用的镜像 +func (r *containerImageRepo) Prune() error { + _, err := r.client.ImagesPrune(context.Background(), filters.NewArgs()) + return err +} + +// Inspect 查看镜像 +func (r *containerImageRepo) Inspect(id string) (types.ImageInspect, error) { + img, _, err := r.client.ImageInspectWithRaw(context.Background(), id) + return img, err +} diff --git a/internal/data/container_network.go b/internal/data/container_network.go new file mode 100644 index 00000000..b44e9985 --- /dev/null +++ b/internal/data/container_network.go @@ -0,0 +1,104 @@ +package data + +import ( + "context" + + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/types" +) + +type containerNetworkRepo struct { + client *client.Client +} + +func NewContainerNetworkRepo(sock ...string) biz.ContainerNetworkRepo { + if len(sock) == 0 { + sock = append(sock, "/run/podman/podman.sock") + } + cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) + return &containerNetworkRepo{ + client: cli, + } +} + +// List 列出网络 +func (r *containerNetworkRepo) List() ([]network.Inspect, error) { + return r.client.NetworkList(context.Background(), network.ListOptions{}) +} + +// Create 创建网络 +func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (string, error) { + var ipamConfigs []network.IPAMConfig + if req.Ipv4.Enabled { + ipamConfigs = append(ipamConfigs, network.IPAMConfig{ + Subnet: req.Ipv4.Subnet, + Gateway: req.Ipv4.Gateway, + IPRange: req.Ipv4.IPRange, + }) + } + if req.Ipv6.Enabled { + ipamConfigs = append(ipamConfigs, network.IPAMConfig{ + Subnet: req.Ipv6.Subnet, + Gateway: req.Ipv6.Gateway, + IPRange: req.Ipv6.IPRange, + }) + } + + options := network.CreateOptions{ + EnableIPv6: &req.Ipv6.Enabled, + Driver: req.Driver, + Options: types.KVToMap(req.Options), + Labels: types.KVToMap(req.Labels), + } + if len(ipamConfigs) > 0 { + options.IPAM = &network.IPAM{ + Config: ipamConfigs, + } + } + + resp, err := r.client.NetworkCreate(context.Background(), req.Name, options) + return resp.ID, err +} + +// Remove 删除网络 +func (r *containerNetworkRepo) Remove(id string) error { + return r.client.NetworkRemove(context.Background(), id) +} + +// Exist 判断网络是否存在 +func (r *containerNetworkRepo) Exist(name string) (bool, error) { + var options network.ListOptions + options.Filters = filters.NewArgs(filters.Arg("name", name)) + networks, err := r.client.NetworkList(context.Background(), options) + if err != nil { + return false, err + } + + return len(networks) > 0, nil +} + +// Inspect 查看网络 +func (r *containerNetworkRepo) Inspect(id string) (network.Inspect, error) { + return r.client.NetworkInspect(context.Background(), id, network.InspectOptions{}) +} + +// Connect 连接网络 +func (r *containerNetworkRepo) Connect(networkID string, containerID string) error { + return r.client.NetworkConnect(context.Background(), networkID, containerID, nil) +} + +// Disconnect 断开网络 +func (r *containerNetworkRepo) Disconnect(networkID string, containerID string) error { + return r.client.NetworkDisconnect(context.Background(), networkID, containerID, true) +} + +// Prune 清理未使用的网络 +func (r *containerNetworkRepo) Prune() error { + _, err := r.client.NetworksPrune(context.Background(), filters.NewArgs()) + return err +} diff --git a/internal/data/container_volume.go b/internal/data/container_volume.go new file mode 100644 index 00000000..a6db2181 --- /dev/null +++ b/internal/data/container_volume.go @@ -0,0 +1,74 @@ +package data + +import ( + "context" + + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/types" +) + +type containerVolumeRepo struct { + client *client.Client +} + +func NewContainerVolumeRepo(sock ...string) biz.ContainerVolumeRepo { + if len(sock) == 0 { + sock = append(sock, "/run/podman/podman.sock") + } + cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) + return &containerVolumeRepo{ + client: cli, + } +} + +// List 列出存储卷 +func (r *containerVolumeRepo) List() ([]*volume.Volume, error) { + volumes, err := r.client.VolumeList(context.Background(), volume.ListOptions{}) + if err != nil { + return nil, err + } + return volumes.Volumes, err +} + +// Create 创建存储卷 +func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (volume.Volume, error) { + return r.client.VolumeCreate(context.Background(), volume.CreateOptions{ + Name: req.Name, + Driver: req.Driver, + DriverOpts: types.KVToMap(req.Options), + Labels: types.KVToMap(req.Labels), + }) +} + +// Exist 判断存储卷是否存在 +func (r *containerVolumeRepo) Exist(id string) (bool, error) { + var options volume.ListOptions + options.Filters = filters.NewArgs(filters.Arg("name", id)) + volumes, err := r.client.VolumeList(context.Background(), options) + if err != nil { + return false, err + } + + return len(volumes.Volumes) > 0, nil +} + +// Inspect 查看存储卷 +func (r *containerVolumeRepo) Inspect(id string) (volume.Volume, error) { + return r.client.VolumeInspect(context.Background(), id) +} + +// Remove 删除存储卷 +func (r *containerVolumeRepo) Remove(id string) error { + return r.client.VolumeRemove(context.Background(), id, true) +} + +// Prune 清理未使用的存储卷 +func (r *containerVolumeRepo) Prune() error { + _, err := r.client.VolumesPrune(context.Background(), filters.NewArgs()) + return err +} diff --git a/internal/data/cron.go b/internal/data/cron.go new file mode 100644 index 00000000..df31f2b3 --- /dev/null +++ b/internal/data/cron.go @@ -0,0 +1,253 @@ +package data + +import ( + "errors" + "fmt" + "path/filepath" + "regexp" + "strconv" + + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/os" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type cronRepo struct { + settingRepo biz.SettingRepo +} + +func NewCronRepo() biz.CronRepo { + return &cronRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r *cronRepo) Count() (int64, error) { + var count int64 + if err := app.Orm.Model(&biz.Cron{}).Count(&count).Error; err != nil { + return 0, err + } + + return count, nil +} + +func (r *cronRepo) List(page, limit uint) ([]*biz.Cron, int64, error) { + var cron []*biz.Cron + var total int64 + err := app.Orm.Model(&biz.Cert{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&cron).Error + return cron, total, err +} + +func (r *cronRepo) Get(id uint) (*biz.Cron, error) { + cron := new(biz.Cron) + if err := app.Orm.Where("id = ?", id).First(cron).Error; err != nil { + return nil, err + } + + return cron, nil +} + +func (r *cronRepo) Create(req *request.CronCreate) error { + if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) { + return errors.New("时间格式错误") + } + + var script string + if req.Type == "backup" { + if len(req.BackupPath) == 0 { + req.BackupPath, _ = r.settingRepo.Get(biz.SettingKeyBackupPath) + if len(req.BackupPath) == 0 { + return errors.New("备份路径不能为空") + } + req.BackupPath = filepath.Join(req.BackupPath, req.BackupType) + } + script = fmt.Sprintf(`#!/bin/bash +export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH + +# 耗子面板 - 数据备份脚本 + +type=%s +path=%s +name=%s +save=%d + +# 执行备份 +panel backup ${type} ${name} ${path} ${save} 2>&1 +`, req.BackupType, req.BackupPath, req.Target, req.Save) + } + if req.Type == "cutoff" { + script = fmt.Sprintf(`#!/bin/bash +export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH + +# 耗子面板 - 日志切割脚本 + +name=%s +save=%d + +# 执行切割 +panel cutoff ${name} ${save} 2>&1 +`, req.Target, req.Save) + } + + shellDir := fmt.Sprintf("%s/server/cron/", app.Root) + shellLogDir := fmt.Sprintf("%s/server/cron/logs/", app.Root) + if !io.Exists(shellDir) { + return errors.New("计划任务目录不存在") + } + if !io.Exists(shellLogDir) { + return errors.New("计划任务日志目录不存在") + } + shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + str.RandomString(16) + if err := io.Write(filepath.Join(shellDir, shellFile+".sh"), script, 0700); err != nil { + return errors.New(err.Error()) + } + if out, err := shell.Execf("dos2unix %s%s.sh", shellDir, shellFile); err != nil { + return errors.New(out) + } + + cron := new(biz.Cron) + cron.Name = req.Name + cron.Type = req.Type + cron.Status = true + cron.Time = req.Time + cron.Shell = shellDir + shellFile + ".sh" + cron.Log = shellLogDir + shellFile + ".log" + + if err := app.Orm.Create(cron).Error; err != nil { + return err + } + if err := r.addToSystem(cron); err != nil { + return err + } + + return nil +} + +func (r *cronRepo) Update(req *request.CronUpdate) error { + cron, err := r.Get(req.ID) + if err != nil { + return err + } + + if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) { + return errors.New("时间格式错误") + } + + if !cron.Status { + return errors.New("计划任务已禁用") + } + + cron.Time = req.Time + cron.Name = req.Name + if err = app.Orm.Save(cron).Error; err != nil { + return err + } + + if err = io.Write(cron.Shell, req.Script, 0700); err != nil { + return err + } + if out, err := shell.Execf("dos2unix %s", cron.Shell); err != nil { + return errors.New(out) + } + + if err = r.deleteFromSystem(cron); err != nil { + return err + } + if cron.Status { + if err = r.addToSystem(cron); err != nil { + return err + } + } + + return nil +} + +func (r *cronRepo) Delete(id uint) error { + cron, err := r.Get(id) + if err != nil { + return err + } + + if err = r.deleteFromSystem(cron); err != nil { + return err + } + if err = io.Remove(cron.Shell); err != nil { + return err + } + + return app.Orm.Delete(cron).Error +} + +func (r *cronRepo) Status(id uint, status bool) error { + cron, err := r.Get(id) + if err != nil { + return err + } + + if err = r.deleteFromSystem(cron); err != nil { + return err + } + if status { + return r.addToSystem(cron) + } + + cron.Status = status + + return app.Orm.Save(cron).Error +} + +func (r *cronRepo) Log(id uint) (string, error) { + cron, err := r.Get(id) + if err != nil { + return "", err + } + + if !io.Exists(cron.Log) { + return "", errors.New("日志文件不存在") + } + + log, err := shell.Execf("tail -n 1000 '%s'", cron.Log) + if err != nil { + return "", err + } + + return log, nil +} + +// addToSystem 添加到系统 +func (r *cronRepo) addToSystem(cron *biz.Cron) error { + if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil { + return err + } + + return r.restartCron() +} + +// deleteFromSystem 从系统中删除 +func (r *cronRepo) deleteFromSystem(cron *biz.Cron) error { + if _, err := shell.Execf(`( crontab -l | grep -v -F "%s %s >> %s 2>&1" ) | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil { + return err + } + + return r.restartCron() +} + +// restartCron 重启 cron 服务 +func (r *cronRepo) restartCron() error { + if os.IsRHEL() { + return systemctl.Restart("crond") + } + + if os.IsDebian() || os.IsUbuntu() { + return systemctl.Restart("cron") + } + + return errors.New("不支持的系统") +} diff --git a/internal/data/monitor.go b/internal/data/monitor.go new file mode 100644 index 00000000..48c53048 --- /dev/null +++ b/internal/data/monitor.go @@ -0,0 +1,67 @@ +package data + +import ( + "errors" + + "github.com/golang-module/carbon/v2" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type monitorRepo struct { + settingRepo biz.SettingRepo +} + +func NewMonitorRepo() biz.MonitorRepo { + return &monitorRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r monitorRepo) GetSetting() (*request.MonitorSetting, error) { + monitor, err := r.settingRepo.Get(biz.SettingKeyMonitor) + if err != nil { + return nil, err + } + monitorDays, err := r.settingRepo.Get(biz.SettingKeyMonitorDays) + if err != nil { + return nil, err + } + + setting := new(request.MonitorSetting) + setting.Enabled = cast.ToBool(monitor) + setting.Days = cast.ToInt(monitorDays) + + return setting, nil +} + +func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error { + if err := r.settingRepo.Set(biz.SettingKeyMonitor, cast.ToString(setting.Enabled)); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeyMonitorDays, cast.ToString(setting.Days)); err != nil { + return err + } + + return nil +} + +func (r monitorRepo) Clear() error { + return app.Orm.Delete(&biz.Monitor{}).Error +} + +func (r monitorRepo) List(start, end carbon.Carbon) ([]*biz.Monitor, error) { + var monitors []*biz.Monitor + if err := app.Orm.Where("created_at BETWEEN ? AND ?", start, end).Find(&monitors).Error; err != nil { + return nil, err + } + + if len(monitors) == 0 { + return nil, errors.New("没有找到数据") + } + + return monitors, nil +} diff --git a/internal/data/plugin.go b/internal/data/plugin.go new file mode 100644 index 00000000..b3dbe655 --- /dev/null +++ b/internal/data/plugin.go @@ -0,0 +1,226 @@ +package data + +import ( + "errors" + "fmt" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/job" + "github.com/TheTNB/panel/pkg/pluginloader" + "github.com/TheTNB/panel/pkg/types" +) + +type pluginRepo struct { + taskRepo biz.TaskRepo +} + +func NewPluginRepo() biz.PluginRepo { + return &pluginRepo{ + taskRepo: NewTaskRepo(), + } +} + +func (r *pluginRepo) All() []*types.Plugin { + return pluginloader.All() +} + +func (r *pluginRepo) Installed() ([]*biz.Plugin, error) { + var plugins []*biz.Plugin + if err := app.Orm.Find(&plugins).Error; err != nil { + return nil, err + } + + return plugins, nil + +} + +func (r *pluginRepo) Get(slug string) (*types.Plugin, error) { + return pluginloader.Get(slug) +} + +func (r *pluginRepo) GetInstalled(slug string) (*biz.Plugin, error) { + plugin := new(biz.Plugin) + if err := app.Orm.Where("slug = ?", slug).First(plugin).Error; err != nil { + return nil, err + } + + return plugin, nil +} + +func (r *pluginRepo) GetInstalledAll(cond ...string) ([]*biz.Plugin, error) { + var plugins []*biz.Plugin + if err := app.Orm.Where(cond).Find(&plugins).Error; err != nil { + return nil, err + } + + return plugins, nil +} + +func (r *pluginRepo) IsInstalled(cond ...string) (bool, error) { + var count int64 + if err := app.Orm.Model(&biz.Plugin{}).Where(cond).Count(&count).Error; err != nil { + return false, err + } + + return count > 0, nil +} + +func (r *pluginRepo) Install(slug string) error { + plugin, err := r.Get(slug) + if err != nil { + return err + } + installedPlugins, err := r.Installed() + if err != nil { + return err + } + + if installed, _ := r.IsInstalled("slug = ?", slug); installed { + return errors.New("插件已安装") + } + + pluginsMap := make(map[string]bool) + + for _, p := range installedPlugins { + pluginsMap[p.Slug] = true + } + + for _, require := range plugin.Requires { + _, requireFound := pluginsMap[require] + if !requireFound { + return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") + } + } + + for _, exclude := range plugin.Excludes { + _, excludeFound := pluginsMap[exclude] + if excludeFound { + return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") + } + } + + task := new(biz.Task) + task.Name = "安装插件 " + plugin.Name + task.Status = biz.TaskStatusWaiting + task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", plugin.Install, plugin.Slug) + task.Log = "/tmp/" + plugin.Slug + ".log" + + if err = app.Orm.Create(task).Error; err != nil { + return err + } + err = app.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ + task.ID, + }) + + return err +} + +func (r *pluginRepo) Uninstall(slug string) error { + plugin, err := r.Get(slug) + if err != nil { + return err + } + installedPlugins, err := r.Installed() + if err != nil { + return err + } + + if installed, _ := r.IsInstalled("slug = ?", slug); !installed { + return errors.New("插件未安装") + } + pluginsMap := make(map[string]bool) + + for _, p := range installedPlugins { + pluginsMap[p.Slug] = true + } + + for _, require := range plugin.Requires { + _, requireFound := pluginsMap[require] + if !requireFound { + return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") + } + } + + for _, exclude := range plugin.Excludes { + _, excludeFound := pluginsMap[exclude] + if excludeFound { + return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") + } + } + + task := new(biz.Task) + task.Name = "卸载插件 " + plugin.Name + task.Status = biz.TaskStatusWaiting + task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", plugin.Uninstall, plugin.Slug) + task.Log = "/tmp/" + plugin.Slug + ".log" + + if err = app.Orm.Create(task).Error; err != nil { + return err + } + err = app.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ + task.ID, + }) + + return err +} + +func (r *pluginRepo) Update(slug string) error { + plugin, err := r.Get(slug) + if err != nil { + return err + } + installedPlugins, err := r.Installed() + if err != nil { + return err + } + + if installed, _ := r.IsInstalled("slug = ?", slug); !installed { + return errors.New("插件未安装") + } + pluginsMap := make(map[string]bool) + + for _, p := range installedPlugins { + pluginsMap[p.Slug] = true + } + + for _, require := range plugin.Requires { + _, requireFound := pluginsMap[require] + if !requireFound { + return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") + } + } + + for _, exclude := range plugin.Excludes { + _, excludeFound := pluginsMap[exclude] + if excludeFound { + return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") + } + } + + task := new(biz.Task) + task.Name = "更新插件 " + plugin.Name + task.Status = biz.TaskStatusWaiting + task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", plugin.Update, plugin.Slug) + task.Log = "/tmp/" + plugin.Slug + ".log" + + if err = app.Orm.Create(task).Error; err != nil { + return err + } + err = app.Queue.Push(job.NewProcessTask(r.taskRepo), []any{ + task.ID, + }) + + return err +} + +func (r *pluginRepo) UpdateShow(slug string, show bool) error { + plugin, err := r.GetInstalled(slug) + if err != nil { + return err + } + + plugin.Show = show + + return app.Orm.Save(plugin).Error +} diff --git a/internal/data/safe.go b/internal/data/safe.go new file mode 100644 index 00000000..26cd66a7 --- /dev/null +++ b/internal/data/safe.go @@ -0,0 +1,120 @@ +package data + +import ( + "errors" + "strings" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/os" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type safeRepo struct { + ssh string +} + +func NewSafeRepo() biz.SafeRepo { + var ssh string + if os.IsRHEL() { + ssh = "sshd" + } else { + ssh = "ssh" + } + + return &safeRepo{ + ssh: ssh, + } +} + +func (r *safeRepo) GetSSH() (uint, bool, error) { + out, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") + if err != nil { + return 0, false, err + } + + running, err := systemctl.Status(r.ssh) + if err != nil { + return 0, false, err + } + + return cast.ToUint(out), running, nil +} + +func (r *safeRepo) UpdateSSH(port uint, status bool) error { + oldPort, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'") + if err != nil { + return err + } + + _, _ = shell.Execf("sed -i 's/#Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port) + _, _ = shell.Execf("sed -i 's/Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port) + + if !status { + return systemctl.Stop(r.ssh) + } + + return systemctl.Restart(r.ssh) +} + +func (r *safeRepo) GetPingStatus() (bool, error) { + if os.IsRHEL() { + out, err := shell.Execf(`firewall-cmd --list-all`) + if err != nil { + return true, errors.New(out) + } + + if !strings.Contains(out, `rule protocol value="icmp" drop`) { + return true, nil + } else { + return false, nil + } + } else { + config, err := io.Read("/etc/ufw/before.rules") + if err != nil { + return true, err + } + if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") { + return true, nil + } else { + return false, nil + } + } +} + +func (r *safeRepo) UpdatePingStatus(status bool) error { + var out string + var err error + if os.IsRHEL() { + if status { + out, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`) + } else { + out, err = shell.Execf(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`) + } + } else { + if status { + out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`) + } else { + out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`) + } + } + + if err != nil { + return errors.New(out) + } + + if os.IsRHEL() { + out, err = shell.Execf(`firewall-cmd --reload`) + } else { + out, err = shell.Execf(`ufw reload`) + } + + if err != nil { + return errors.New(out) + } + + return nil +} diff --git a/internal/data/setting.go b/internal/data/setting.go new file mode 100644 index 00000000..42b0b68e --- /dev/null +++ b/internal/data/setting.go @@ -0,0 +1,68 @@ +package data + +import ( + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type settingRepo struct{} + +func NewSettingRepo() biz.SettingRepo { + return &settingRepo{} +} + +func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil { + return "", err + } + + if setting.Value == "" && len(defaultValue) > 0 { + return defaultValue[0], nil + } + + return setting.Value, nil +} + +func (r *settingRepo) Set(key biz.SettingKey, value string) error { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil { + return err + } + + setting.Value = value + return app.Orm.Save(setting).Error +} + +func (r *settingRepo) Delete(key biz.SettingKey) error { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", key).Delete(setting).Error; err != nil { + return err + } + + return nil +} + +func (r *settingRepo) GetPanelSetting() (*request.PanelSetting, error) { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(setting).Error; err != nil { + return nil, err + } + + // TODO fix + + return &request.PanelSetting{ + Name: setting.Value, + }, nil +} + +func (r *settingRepo) UpdatePanelSetting(setting *request.PanelSetting) error { + if err := r.Set(biz.SettingKeyName, setting.Name); err != nil { + return err + } + + // TODO fix + + return nil +} diff --git a/internal/data/ssh.go b/internal/data/ssh.go new file mode 100644 index 00000000..165ba55e --- /dev/null +++ b/internal/data/ssh.go @@ -0,0 +1,54 @@ +package data + +import ( + "errors" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type sshRepo struct { + settingRepo biz.SettingRepo +} + +func NewSSHRepo() biz.SSHRepo { + return &sshRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r *sshRepo) GetInfo() (map[string]any, error) { + host, _ := r.settingRepo.Get(biz.SettingKeySshHost) + port, _ := r.settingRepo.Get(biz.SettingKeySshPort) + user, _ := r.settingRepo.Get(biz.SettingKeySshUser) + password, _ := r.settingRepo.Get(biz.SettingKeySshPassword) + if len(host) == 0 || len(user) == 0 || len(password) == 0 { + return nil, errors.New("SSH 配置不完整") + } + + return map[string]any{ + "host": host, + "port": cast.ToInt(port), + "user": user, + "password": password, + }, nil +} + +func (r *sshRepo) UpdateInfo(req *request.SSHUpdateInfo) error { + if err := r.settingRepo.Set(biz.SettingKeySshHost, req.Host); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeySshPort, req.Port); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeySshUser, req.User); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeySshPassword, req.Password); err != nil { + return err + } + + return nil +} diff --git a/internal/data/task.go b/internal/data/task.go new file mode 100644 index 00000000..82fdc5bd --- /dev/null +++ b/internal/data/task.go @@ -0,0 +1,39 @@ +package data + +import ( + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" +) + +type taskRepo struct{} + +func NewTaskRepo() biz.TaskRepo { + return &taskRepo{} +} + +func (r *taskRepo) HasRunningTask() bool { + var count int64 + app.Orm.Model(&biz.Task{}).Where("status = ?", biz.TaskStatusRunning).Or("status = ?", biz.TaskStatusWaiting).Count(&count) + return count > 0 +} + +func (r *taskRepo) List(page, limit uint) ([]*biz.Task, int64, error) { + var tasks []*biz.Task + var total int64 + err := app.Orm.Model(&biz.Task{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&tasks).Error + return tasks, total, err +} + +func (r *taskRepo) Get(id uint) (*biz.Task, error) { + task := new(biz.Task) + err := app.Orm.Model(&biz.Task{}).Where("id = ?", id).First(task).Error + return task, err +} + +func (r *taskRepo) Delete(id uint) error { + return app.Orm.Model(&biz.Task{}).Where("id = ?", id).Delete(&biz.Task{}).Error +} + +func (r *taskRepo) UpdateStatus(id uint, status biz.TaskStatus) error { + return app.Orm.Model(&biz.Task{}).Where("id = ?", id).Update("status", status).Error +} diff --git a/internal/data/user.go b/internal/data/user.go new file mode 100644 index 00000000..79b45b13 --- /dev/null +++ b/internal/data/user.go @@ -0,0 +1,51 @@ +package data + +import ( + "errors" + + "github.com/go-rat/utils/hash" + "gorm.io/gorm" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" +) + +type userRepo struct { + hasher hash.Hasher +} + +func NewUserRepo() biz.UserRepo { + return &userRepo{ + hasher: hash.NewArgon2id(), + } +} + +func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) { + user := new(biz.User) + if err := app.Orm.Where("username = ?", username).First(user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("用户名或密码错误") + } else { + return nil, err + } + } + + if !r.hasher.Check(password, user.Password) { + return nil, errors.New("用户名或密码错误") + } + + return user, nil +} + +func (r *userRepo) Get(id uint) (*biz.User, error) { + user := new(biz.User) + if err := app.Orm.First(user, id).Error; err != nil { + return nil, err + } + + return user, nil +} + +func (r *userRepo) Save(user *biz.User) error { + return app.Orm.Save(user).Error +} diff --git a/internal/data/website.go b/internal/data/website.go new file mode 100644 index 00000000..31e62190 --- /dev/null +++ b/internal/data/website.go @@ -0,0 +1,784 @@ +package data + +import ( + "errors" + "fmt" + "path/filepath" + "regexp" + "slices" + "strconv" + "strings" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/embed" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/cert" + "github.com/TheTNB/panel/pkg/db" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" + "github.com/TheTNB/panel/pkg/systemctl" + "github.com/TheTNB/panel/pkg/types" +) + +type websiteRepo struct { + settingRepo biz.SettingRepo +} + +func NewWebsiteRepo() biz.WebsiteRepo { + return &websiteRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r *websiteRepo) UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error { + if err := io.Write(filepath.Join(app.Root, "server/openresty/html/index.html"), req.Index, 0644); err != nil { + return err + } + if err := io.Write(filepath.Join(app.Root, "server/openresty/html/stop.html"), req.Stop, 0644); err != nil { + return err + } + + return systemctl.Reload("openresty") +} + +func (r *websiteRepo) Count() (int64, error) { + var count int64 + if err := app.Orm.Model(&biz.Website{}).Count(&count).Error; err != nil { + return 0, err + } + + return count, nil +} + +func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(website).Error; err != nil { + return nil, err + } + + config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + if err != nil { + return nil, err + } + + setting := new(types.WebsiteSetting) + setting.Name = website.Name + setting.Path = website.Path + setting.SSL = website.SSL + setting.PHP = strconv.Itoa(website.PHP) + setting.Raw = config + + portStr := str.Cut(config, "# port标记位开始", "# port标记位结束") + matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1) + for _, match := range matches { + if len(match) < 2 { + continue + } + // 跳过 ipv6 + if strings.Contains(match[1], "[::]") { + continue + } + + // 处理 443 ssl 之类的情况 + ports := strings.Fields(match[1]) + if len(ports) == 0 { + continue + } + if !slices.Contains(setting.Ports, ports[0]) { + setting.Ports = append(setting.Ports, ports[0]) + } + if len(ports) > 1 && ports[1] == "ssl" { + setting.SSLPorts = append(setting.SSLPorts, ports[0]) + } else if len(ports) > 1 && ports[1] == "quic" { + setting.QUICPorts = append(setting.QUICPorts, ports[0]) + } + } + serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束") + match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName) + if len(match) > 1 { + setting.Domains = strings.Split(match[1], " ") + } + root := str.Cut(config, "# root标记位开始", "# root标记位结束") + match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root) + if len(match) > 1 { + setting.Root = match[1] + } + index := str.Cut(config, "# index标记位开始", "# index标记位结束") + match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index) + if len(match) > 1 { + setting.Index = match[1] + } + + if io.Exists(filepath.Join(setting.Root, ".user.ini")) { + userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini")) + if strings.Contains(userIni, "open_basedir") { + setting.OpenBasedir = true + } + } + + crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem")) + setting.SSLCertificate = crt + key, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key")) + setting.SSLCertificateKey = key + if setting.SSL { + ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束") + setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位") + setting.HSTS = strings.Contains(ssl, "# hsts标记位") + setting.OCSP = strings.Contains(ssl, "# ocsp标记位") + } + + // 解析证书信息 + if decode, err := cert.ParseCert(crt); err == nil { + setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05") + setting.SSLNotAfter = decode.NotAfter.Format("2006-01-02 15:04:05") + setting.SSLIssuer = decode.Issuer.CommonName + setting.SSLOCSPServer = decode.OCSPServer + setting.SSLDNSNames = decode.DNSNames + } + + waf := str.Cut(config, "# waf标记位开始", "# waf标记位结束") + setting.Waf = strings.Contains(waf, "waf on;") + match = regexp.MustCompile(`waf_mode\s+([^;]*);?`).FindStringSubmatch(waf) + if len(match) > 1 { + setting.WafMode = match[1] + } + match = regexp.MustCompile(`waf_cc_deny\s+([^;]*);?`).FindStringSubmatch(waf) + if len(match) > 1 { + setting.WafCcDeny = match[1] + } + match = regexp.MustCompile(`waf_cache\s+([^;]*);?`).FindStringSubmatch(waf) + if len(match) > 1 { + setting.WafCache = match[1] + } + + rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf")) + setting.Rewrite = rewrite + log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name) + setting.Log = log + + return setting, err +} + +func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) { + var websites []*biz.Website + var total int64 + + if err := app.Orm.Model(&biz.Website{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if err := app.Orm.Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&websites).Error; err != nil { + return nil, 0, err + } + + return websites, total, nil +} + +func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) { + w := &biz.Website{ + Name: req.Name, + Status: true, + Path: req.Path, + PHP: cast.ToInt(req.PHP), + SSL: false, + } + if err := app.Orm.Create(w).Error; err != nil { + return nil, err + } + + if err := io.Mkdir(req.Path, 0755); err != nil { + return nil, err + } + + index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html")) + if err != nil { + return nil, fmt.Errorf("获取index模板文件失败: %w", err) + } + if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil { + return nil, err + } + + notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html")) + if err != nil { + return nil, fmt.Errorf("获取404模板文件失败: %w", err) + } + if err = io.Write(req.Path+"/404.html", string(notFound), 0644); err != nil { + return nil, err + } + + portList := "" + domainList := "" + portUsed := make(map[uint]bool) + domainUsed := make(map[string]bool) + + for i, port := range req.Ports { + if _, ok := portUsed[port]; !ok { + if i == len(req.Ports)-1 { + portList += " listen " + cast.ToString(port) + ";\n" + portList += " listen [::]:" + cast.ToString(port) + ";" + } else { + portList += " listen " + cast.ToString(port) + ";\n" + portList += " listen [::]:" + cast.ToString(port) + ";\n" + } + portUsed[port] = true + } + } + for _, domain := range req.Domains { + if _, ok := domainUsed[domain]; !ok { + domainList += " " + domain + domainUsed[domain] = true + } + } + + nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! +# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 +server +{ + # port标记位开始 +%s + # port标记位结束 + # server_name标记位开始 + server_name%s; + # server_name标记位结束 + # index标记位开始 + index index.php index.html; + # index标记位结束 + # root标记位开始 + root %s; + # root标记位结束 + + # ssl标记位开始 + # ssl标记位结束 + + # php标记位开始 + include enable-php-%s.conf; + # php标记位结束 + + # waf标记位开始 + waf off; + waf_rule_path %s/server/openresty/ngx_waf/assets/rules/; + waf_mode DYNAMIC; + waf_cc_deny rate=1000r/m duration=60m; + waf_cache capacity=50; + # waf标记位结束 + + # 错误页配置,可自行设置 + error_page 404 /404.html; + #error_page 502 /502.html; + + # acme证书签发配置,不可修改 + include %s/server/vhost/acme/%s.conf; + + # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 + include %s/server/vhost/rewrite/%s.conf; + + # 面板默认禁止访问部分敏感目录,可自行修改 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) + { + return 404; + } + # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 + location ~ .*\.(js|css)$ + { + expires 1h; + error_log /dev/null; + access_log /dev/null; + } + + access_log %s/wwwlogs/%s.log; + error_log %s/wwwlogs/%s.log; +} +`, portList, domainList, req.Path, req.PHP, app.Root, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name) + + if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), nginxConf, 0644); err != nil { + return nil, err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil { + return nil, err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil { + return nil, err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".pem"), "", 0644); err != nil { + return nil, err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".key"), "", 0644); err != nil { + return nil, err + } + + if err = io.Chmod(req.Path, 0755); err != nil { + return nil, err + } + if err = io.Chown(req.Path, "www", "www"); err != nil { + return nil, err + } + + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return nil, err + } + + rootPassword, err := r.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + if err == nil && req.DB && req.DBType == "mysql" { + mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") + if err != nil { + return nil, err + } + if err = mysql.DatabaseCreate(req.DBName); err != nil { + return nil, err + } + if err = mysql.UserCreate(req.DBUser, req.DBPassword); err != nil { + return nil, err + } + if err = mysql.PrivilegesGrant(req.DBUser, req.DBName); err != nil { + return nil, err + } + } + if req.DB && req.DBType == "postgresql" { + _, _ = shell.Execf(`echo "CREATE DATABASE '%s';" | su - postgres -c "psql"`, req.DBName) + _, _ = shell.Execf(`echo "CREATE USER '%s' WITH PASSWORD '%s';" | su - postgres -c "psql"`, req.DBUser, req.DBPassword) + _, _ = shell.Execf(`echo "ALTER DATABASE '%s' OWNER TO '%s';" | su - postgres -c "psql"`, req.DBName, req.DBUser) + _, _ = shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE '%s' TO '%s';" | su - postgres -c "psql"`, req.DBName, req.DBUser) + userConfig := "host " + req.DBName + " " + req.DBUser + " 127.0.0.1/32 scram-sha-256" + _, _ = shell.Execf(`echo "`+userConfig+`" >> %s/server/postgresql/data/pg_hba.conf`, app.Root) + _ = systemctl.Reload("postgresql") + } + + return w, nil +} + +func (r *websiteRepo) Update(req *request.WebsiteUpdate) error { + website := new(biz.Website) + if err := app.Orm.Where("id", req.ID).First(website).Error; err != nil { + return err + } + + if !website.Status { + return errors.New("网站已停用,请先启用") + } + + // 原文 + raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + if err != nil { + return err + } + if strings.TrimSpace(raw) != strings.TrimSpace(req.Raw) { + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), req.Raw, 0644); err != nil { + return err + } + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return err + } + + return nil + } + + // 目录 + path := req.Path + if !io.Exists(path) { + return errors.New("网站目录不存在") + } + website.Path = path + + // 域名 + domain := "server_name" + domains := req.Domains + for _, v := range domains { + if v == "" { + continue + } + domain += " " + v + } + domain += ";" + domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束") + if len(strings.TrimSpace(domainConfigOld)) == 0 { + return errors.New("配置文件中缺少server_name标记位") + } + raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1) + + // 端口 + var portConf strings.Builder + ports := req.Ports + for _, port := range ports { + https := "" + quic := false + if slices.Contains(req.SSLPorts, port) { + https = " ssl" + if slices.Contains(req.QUICPorts, port) { + quic = true + } + } + + portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https)) + portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https)) + if quic { + portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic")) + portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic")) + } + } + portConf.WriteString(" ") + portConfNew := portConf.String() + portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束") + if len(strings.TrimSpace(portConfOld)) == 0 { + return errors.New("配置文件中缺少port标记位") + } + raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1) + + // 运行目录 + root := str.Cut(raw, "# root标记位开始", "# root标记位结束") + if len(strings.TrimSpace(root)) == 0 { + return errors.New("配置文件中缺少root标记位") + } + match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root) + if len(match) != 2 { + return errors.New("配置文件中root标记位格式错误") + } + rootNew := strings.Replace(root, match[1], req.Root, -1) + raw = strings.Replace(raw, root, rootNew, -1) + + // 默认文件 + index := str.Cut(raw, "# index标记位开始", "# index标记位结束") + if len(strings.TrimSpace(index)) == 0 { + return errors.New("配置文件中缺少index标记位") + } + match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index) + if len(match) != 2 { + return errors.New("配置文件中index标记位格式错误") + } + indexNew := strings.Replace(index, match[1], req.Index, -1) + raw = strings.Replace(raw, index, indexNew, -1) + + // 防跨站 + if !strings.HasSuffix(req.Root, "/") { + req.Root += "/" + } + if req.OpenBasedir { + if err = io.Write(req.Root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil { + return err + } + } else { + if io.Exists(req.Root + ".user.ini") { + if err = io.Remove(req.Root + ".user.ini"); err != nil { + return err + } + } + } + + // WAF + wafStr := "off" + if req.Waf { + wafStr = "on" + } + wafConfig := fmt.Sprintf(`# waf标记位开始 + waf %s; + waf_rule_path %s/server/openresty/ngx_waf/assets/rules/; + waf_mode %s; + waf_cc_deny %s; + waf_cache %s; + `, wafStr, app.Root, req.WafMode, req.WafCcDeny, req.WafCache) + wafConfigOld := str.Cut(raw, "# waf标记位开始", "# waf标记位结束") + if len(strings.TrimSpace(wafConfigOld)) != 0 { + raw = strings.Replace(raw, wafConfigOld, "", -1) + } + raw = strings.Replace(raw, "# waf标记位开始", wafConfig, -1) + + // SSL + if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"), req.SSLCertificate, 0644); err != nil { + return err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"), req.SSLCertificateKey, 0644); err != nil { + return err + } + website.SSL = req.SSL + if req.SSL { + if _, err = cert.ParseCert(req.SSLCertificate); err != nil { + return errors.New("TLS证书格式错误") + } + if _, err = cert.ParseKey(req.SSLCertificateKey); err != nil { + return errors.New("TLS私钥格式错误") + } + sslConfig := fmt.Sprintf(`# ssl标记位开始 + ssl_certificate %s/server/vhost/ssl/%s.pem; + ssl_certificate_key %s/server/vhost/ssl/%s.key; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_early_data on; + `, app.Root, website.Name, app.Root, website.Name) + if req.HTTPRedirect { + sslConfig += `# http重定向标记位开始 + if ($server_port !~ 443){ + return 301 https://$host$request_uri; + } + error_page 497 https://$host$request_uri; + # http重定向标记位结束 + ` + } + if req.HSTS { + sslConfig += `# hsts标记位开始 + add_header Strict-Transport-Security "max-age=63072000" always; + # hsts标记位结束 + ` + } + if req.OCSP { + sslConfig += `# ocsp标记位开始 + ssl_stapling on; + ssl_stapling_verify on; + # ocsp标记位结束 + ` + } + sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") + if len(strings.TrimSpace(sslConfigOld)) != 0 { + raw = strings.Replace(raw, sslConfigOld, "", -1) + } + raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1) + } else { + sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") + if len(strings.TrimSpace(sslConfigOld)) != 0 { + raw = strings.Replace(raw, sslConfigOld, "\n ", -1) + } + } + + if website.PHP != req.PHP { + website.PHP = req.PHP + phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束") + phpConfig := ` + include enable-php-` + strconv.Itoa(website.PHP) + `.conf; + ` + if len(strings.TrimSpace(phpConfigOld)) != 0 { + raw = strings.Replace(raw, phpConfigOld, phpConfig, -1) + } + } + + if err = app.Orm.Save(website).Error; err != nil { + return err + } + + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { + return err + } + if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), req.Rewrite, 0644); err != nil { + return err + } + + err = systemctl.Reload("openresty") + if err != nil { + _, err = shell.Execf("openresty -t") + } + + return err +} + +func (r *websiteRepo) Delete(req *request.WebsiteDelete) error { + website := new(biz.Website) + if err := app.Orm.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil { + return err + } + + if website.Cert != nil { + return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书") + } + + if err := app.Orm.Delete(website).Error; err != nil { + return err + } + + _ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + _ = io.Remove(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf")) + _ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf")) + _ = io.Remove(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem")) + _ = io.Remove(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key")) + + if req.Path { + _ = io.Remove(website.Path) + } + if req.DB { + rootPassword, err := r.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + if err != nil { + return err + } + mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") + if err != nil { + return err + } + _ = mysql.DatabaseDrop(website.Name) + _ = mysql.UserDrop(website.Name) + _, _ = shell.Execf(`echo "DROP DATABASE IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) + _, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) + } + + err := systemctl.Reload("openresty") + if err != nil { + _, err = shell.Execf("openresty -t") + } + + return err +} + +func (r *websiteRepo) ClearLog(id uint) error { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(website).Error; err != nil { + return err + } + + _, err := shell.Execf(`echo "" > %s/wwwlogs/%s.log`, app.Root, website.Name) + return err +} + +func (r *websiteRepo) UpdateRemark(id uint, remark string) error { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(website).Error; err != nil { + return err + } + + website.Remark = remark + return app.Orm.Save(website).Error +} + +func (r *websiteRepo) ResetConfig(id uint) error { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(&website).Error; err != nil { + return err + } + + website.Status = true + website.SSL = false + if err := app.Orm.Save(website).Error; err != nil { + return err + } + + raw := fmt.Sprintf(` +# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! +# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 +server +{ + # port标记位开始 + listen 80; + # port标记位结束 + # server_name标记位开始 + server_name localhost; + # server_name标记位结束 + # index标记位开始 + index index.php index.html; + # index标记位结束 + # root标记位开始 + root %s; + # root标记位结束 + + # ssl标记位开始 + # ssl标记位结束 + + # php标记位开始 + include enable-php-%d.conf; + # php标记位结束 + + # waf标记位开始 + waf off; + waf_rule_path %s/server/openresty/ngx_waf/assets/rules/; + waf_mode DYNAMIC; + waf_cc_deny rate=1000r/m duration=60m; + waf_cache capacity=50; + # waf标记位结束 + + # 错误页配置,可自行设置 + error_page 404 /404.html; + #error_page 502 /502.html; + + # acme证书签发配置,不可修改 + include %s/server/vhost/acme/%s.conf; + + # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 + include %s/server/vhost/rewrite/%s.conf; + + # 面板默认禁止访问部分敏感目录,可自行修改 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) + { + return 404; + } + # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 + location ~ .*\.(js|css)$ + { + expires 1h; + error_log /dev/null; + access_log /dev/null; + } + + access_log %s/wwwlogs/%s.log; + error_log %s/wwwlogs/%s.log; +} + +`, website.Path, website.PHP, app.Root, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name) + if err := io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { + return nil + } + if err := io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil { + return nil + } + if err := io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil { + return nil + } + if err := systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return err + } + + return nil +} + +func (r *websiteRepo) UpdateStatus(id uint, status bool) error { + website := new(biz.Website) + if err := app.Orm.Where("id", id).First(&website).Error; err != nil { + return err + } + + website.Status = status + if err := app.Orm.Save(website).Error; err != nil { + return err + } + + raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf")) + if err != nil { + return err + } + + // 运行目录 + rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束") + match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig) + if len(match) == 2 { + if website.Status { + root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig) + raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s;\n ", root[1])) + } else { + raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s/server/openresty/html;\n # root %s;\n ", app.Root, match[1])) + } + } + + // 默认文件 + indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束") + match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig) + if len(match) == 2 { + if website.Status { + index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig) + raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ") + } else { + raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ") + } + } + + if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil { + return err + } + if err = systemctl.Reload("openresty"); err != nil { + _, err = shell.Execf("openresty -t") + return err + } + + return nil +} diff --git a/embed/embed.go b/internal/embed/embed.go similarity index 100% rename from embed/embed.go rename to internal/embed/embed.go diff --git a/storage/logs/.gitignore b/internal/embed/frontend/.gitignore similarity index 100% rename from storage/logs/.gitignore rename to internal/embed/frontend/.gitignore diff --git a/embed/website/404.html b/internal/embed/website/404.html similarity index 100% rename from embed/website/404.html rename to internal/embed/website/404.html diff --git a/embed/website/index.html b/internal/embed/website/index.html similarity index 100% rename from embed/website/index.html rename to internal/embed/website/index.html diff --git a/internal/http/middleware/middleware.go b/internal/http/middleware/middleware.go new file mode 100644 index 00000000..7c563bf5 --- /dev/null +++ b/internal/http/middleware/middleware.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "net/http" + + "github.com/go-chi/chi/v5/middleware" + sessionmiddleware "github.com/go-rat/sessions/middleware" + + "github.com/TheTNB/panel/internal/app" +) + +// GlobalMiddleware is a collection of global middleware that will be applied to every request. +func GlobalMiddleware() []func(http.Handler) http.Handler { + return []func(http.Handler) http.Handler{ + sessionmiddleware.StartSession(app.Session), + //middleware.SupressNotFound(app.Http),// bug https://github.com/go-chi/chi/pull/940 + middleware.CleanPath, + middleware.StripSlashes, + middleware.Logger, + middleware.Recoverer, + middleware.Compress(5), + } +} diff --git a/internal/http/middleware/must_login.go b/internal/http/middleware/must_login.go new file mode 100644 index 00000000..77bbd4e5 --- /dev/null +++ b/internal/http/middleware/must_login.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "context" + "net/http" + + "github.com/go-rat/chix" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" +) + +// MustLogin 确保已登录 +func MustLogin(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + session, err := app.Session.GetSession(r) + if err != nil { + render := chix.NewRender(w) + render.Status(http.StatusInternalServerError) + render.JSON(chix.M{ + "message": err.Error(), + }) + } + + if session.Missing("user_id") { + render := chix.NewRender(w) + render.Status(http.StatusUnauthorized) + render.JSON(chix.M{ + "message": "会话已过期,请重新登录", + }) + return + } + + userID := cast.ToUint(session.Get("user_id")) + if userID == 0 { + render := chix.NewRender(w) + render.Status(http.StatusUnauthorized) + render.JSON(chix.M{ + "message": "会话无效,请重新登录", + }) + return + } + + r = r.WithContext(context.WithValue(r.Context(), "user_id", userID)) // nolint:staticcheck + next.ServeHTTP(w, r) + }) +} diff --git a/internal/http/middleware/throttle.go b/internal/http/middleware/throttle.go new file mode 100644 index 00000000..25acfb07 --- /dev/null +++ b/internal/http/middleware/throttle.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "net/http" + "time" + + "github.com/sethvargo/go-limiter/httplimit" + "github.com/sethvargo/go-limiter/memorystore" +) + +// Throttle 限流器 +func Throttle(tokens uint64, interval time.Duration) func(next http.Handler) http.Handler { + store, err := memorystore.New(&memorystore.Config{ + Tokens: tokens, + Interval: interval, + }) + if err != nil { + panic(err) + } + + limiter, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) + if err != nil { + panic(err) + } + + return limiter.Handle +} diff --git a/internal/http/request/cert.go b/internal/http/request/cert.go new file mode 100644 index 00000000..2fe9f9e7 --- /dev/null +++ b/internal/http/request/cert.go @@ -0,0 +1,25 @@ +package request + +type CertCreate struct { + Type string `form:"type" json:"type"` + Domains []string `form:"domains" json:"domains"` + AutoRenew bool `form:"auto_renew" json:"auto_renew"` + AccountID uint `form:"account_id" json:"account_id"` + DNSID uint `form:"dns_id" json:"dns_id"` + WebsiteID uint `form:"website_id" json:"website_id"` +} + +type CertUpdate struct { + ID uint `form:"id" json:"id"` + Type string `form:"type" json:"type"` + Domains []string `form:"domains" json:"domains"` + AutoRenew bool `form:"auto_renew" json:"auto_renew"` + AccountID uint `form:"account_id" json:"account_id"` + DNSID uint `form:"dns_id" json:"dns_id"` + WebsiteID uint `form:"website_id" json:"website_id"` +} + +type CertDeploy struct { + ID uint `form:"id" json:"id"` + WebsiteID uint `form:"website_id" json:"website_id"` +} diff --git a/internal/http/request/cert_account.go b/internal/http/request/cert_account.go new file mode 100644 index 00000000..25468628 --- /dev/null +++ b/internal/http/request/cert_account.go @@ -0,0 +1,18 @@ +package request + +type CertAccountCreate struct { + CA string `form:"ca" json:"ca"` + Email string `form:"email" json:"email"` + Kid string `form:"kid" json:"kid"` + HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` + KeyType string `form:"key_type" json:"key_type"` +} + +type CertAccountUpdate struct { + ID uint `form:"id" json:"id"` + CA string `form:"ca" json:"ca"` + Email string `form:"email" json:"email"` + Kid string `form:"kid" json:"kid"` + HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` + KeyType string `form:"key_type" json:"key_type"` +} diff --git a/internal/http/request/cert_dns.go b/internal/http/request/cert_dns.go new file mode 100644 index 00000000..c01d58fe --- /dev/null +++ b/internal/http/request/cert_dns.go @@ -0,0 +1,16 @@ +package request + +import "github.com/TheTNB/panel/pkg/acme" + +type CertDNSCreate struct { + Type string `form:"type" json:"type"` + Name string `form:"name" json:"name"` + Data acme.DNSParam `form:"data" json:"data"` +} + +type CertDNSUpdate struct { + ID uint `form:"id" json:"id"` + Type string `form:"type" json:"type"` + Name string `form:"name" json:"name"` + Data acme.DNSParam `form:"data" json:"data"` +} diff --git a/internal/http/request/common.go b/internal/http/request/common.go new file mode 100644 index 00000000..b6fe9c3a --- /dev/null +++ b/internal/http/request/common.go @@ -0,0 +1,5 @@ +package request + +type ID struct { + ID uint `json:"id" form:"id" query:"id" validate:"required,number"` +} diff --git a/internal/http/request/container.go b/internal/http/request/container.go new file mode 100644 index 00000000..ba3e1c79 --- /dev/null +++ b/internal/http/request/container.go @@ -0,0 +1,33 @@ +package request + +import "github.com/TheTNB/panel/pkg/types" + +type ContainerID struct { + ID string `json:"id" form:"id"` +} + +type ContainerRename struct { + ID string `form:"id" json:"id"` + Name string `form:"name" json:"name"` +} + +type ContainerCreate struct { + Name string `form:"name" json:"name"` + Image string `form:"image" json:"image"` + Ports []types.ContainerPort `form:"ports" json:"ports"` + Network string `form:"network" json:"network"` + Volumes []types.ContainerVolume `form:"volumes" json:"volumes"` + Labels []types.KV `form:"labels" json:"labels"` + Env []types.KV `form:"env" json:"env"` + Entrypoint []string `form:"entrypoint" json:"entrypoint"` + Command []string `form:"command" json:"command"` + RestartPolicy string `form:"restart_policy" json:"restart_policy"` + AutoRemove bool `form:"auto_remove" json:"auto_remove"` + Privileged bool `form:"privileged" json:"privileged"` + OpenStdin bool `form:"openStdin" json:"open_stdin"` + PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"` + Tty bool `form:"tty" json:"tty"` + CPUShares int64 `form:"cpu_shares" json:"cpu_shares"` + CPUs int64 `form:"cpus" json:"cpus"` + Memory int64 `form:"memory" json:"memory"` +} diff --git a/internal/http/request/container_image.go b/internal/http/request/container_image.go new file mode 100644 index 00000000..e37f9959 --- /dev/null +++ b/internal/http/request/container_image.go @@ -0,0 +1,12 @@ +package request + +type ContainerImageID struct { + ID string `json:"id" form:"id"` +} + +type ContainerImagePull struct { + Name string `form:"name" json:"name"` + Auth bool `form:"auth" json:"auth"` + Username string `form:"username" json:"username"` + Password string `form:"password" json:"password"` +} diff --git a/internal/http/request/container_network.go b/internal/http/request/container_network.go new file mode 100644 index 00000000..78ce93ce --- /dev/null +++ b/internal/http/request/container_network.go @@ -0,0 +1,21 @@ +package request + +import "github.com/TheTNB/panel/pkg/types" + +type ContainerNetworkID struct { + ID string `json:"id" form:"id"` +} + +type ContainerNetworkCreate struct { + Name string `form:"name" json:"name"` + Driver string `form:"driver" json:"driver"` + Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"` + Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"` + Labels []types.KV `form:"labels" json:"labels"` + Options []types.KV `form:"options" json:"options"` +} + +type ContainerNetworkConnect struct { + Network string `form:"network" json:"network"` + Container string `form:"container" json:"container"` +} diff --git a/internal/http/request/container_volume.go b/internal/http/request/container_volume.go new file mode 100644 index 00000000..fab0f60d --- /dev/null +++ b/internal/http/request/container_volume.go @@ -0,0 +1,14 @@ +package request + +import "github.com/TheTNB/panel/pkg/types" + +type ContainerVolumeID struct { + ID string `json:"id" form:"id"` +} + +type ContainerVolumeCreate struct { + Name string `form:"name" json:"name"` + Driver string `form:"driver" json:"driver"` + Labels []types.KV `form:"labels" json:"labels"` + Options []types.KV `form:"options" json:"options"` +} diff --git a/internal/http/request/cron.go b/internal/http/request/cron.go new file mode 100644 index 00000000..02ca39c3 --- /dev/null +++ b/internal/http/request/cron.go @@ -0,0 +1,24 @@ +package request + +type CronCreate struct { + Name string `form:"name" json:"name"` + Type string `form:"type" json:"type"` + Time string `form:"time" json:"time"` + Script string `form:"script" json:"script"` + BackupType string `form:"backup_type" json:"backup_type"` + BackupPath string `form:"backup_path" json:"backup_path"` + Target string `form:"target" json:"target"` + Save int `form:"save" json:"save"` +} + +type CronUpdate struct { + ID uint `form:"id" json:"id"` + Name string `form:"name" json:"name"` + Time string `form:"time" json:"time"` + Script string `form:"script" json:"script"` +} + +type CronStatus struct { + ID uint `form:"id" json:"id"` + Status bool `form:"status" json:"status"` +} diff --git a/internal/http/request/file.go b/internal/http/request/file.go new file mode 100644 index 00000000..141dd062 --- /dev/null +++ b/internal/http/request/file.go @@ -0,0 +1,54 @@ +package request + +type FilePath struct { + Path string `json:"path" form:"path"` +} + +type FileCreate struct { + Dir bool `json:"dir" form:"dir"` + Path string `json:"path" form:"path"` +} + +type FileSave struct { + Path string `form:"path" json:"path"` + Content string `form:"content" json:"content"` +} + +type FileUpload struct { + Path string `json:"path" form:"path"` + File []byte `json:"file" form:"file"` +} + +type FileMove struct { + Source string `form:"source" json:"source"` + Target string `form:"target" json:"target"` + Force bool `form:"force" json:"force"` +} + +type FileCopy struct { + Source string `form:"source" json:"source"` + Target string `form:"target" json:"target"` + Force bool `form:"force" json:"force"` +} + +type FilePermission struct { + Path string `form:"path" json:"path"` + Mode string `form:"mode" json:"mode"` + Owner string `form:"owner" json:"owner"` + Group string `form:"group" json:"group"` +} + +type FileCompress struct { + Paths []string `form:"paths" json:"paths"` + File string `form:"file" json:"file"` +} + +type FileUnCompress struct { + File string `form:"file" json:"file"` + Path string `form:"path" json:"path"` +} + +type FileSearch struct { + Path string `form:"path" json:"path"` + KeyWord string `form:"keyword" json:"keyword"` +} diff --git a/internal/http/request/firewall.go b/internal/http/request/firewall.go new file mode 100644 index 00000000..f20fcb31 --- /dev/null +++ b/internal/http/request/firewall.go @@ -0,0 +1,10 @@ +package request + +type FirewallStatus struct { + Status bool `json:"status" form:"status"` +} + +type FirewallCreateRule struct { + Port uint `json:"port"` + Protocol string `json:"protocol"` +} diff --git a/internal/http/request/monitor.go b/internal/http/request/monitor.go new file mode 100644 index 00000000..82a1543d --- /dev/null +++ b/internal/http/request/monitor.go @@ -0,0 +1,11 @@ +package request + +type MonitorSetting struct { + Enabled bool `json:"enabled"` + Days int `json:"days"` +} + +type MonitorList struct { + Start int64 `json:"start"` + End int64 `json:"end"` +} diff --git a/internal/http/request/paginate.go b/internal/http/request/paginate.go new file mode 100644 index 00000000..eb200cf4 --- /dev/null +++ b/internal/http/request/paginate.go @@ -0,0 +1,32 @@ +package request + +import ( + "net/http" +) + +type Paginate struct { + Page uint `json:"page" form:"page" query:"page" validate:"required,number,gte=1"` + Limit uint `json:"limit" form:"limit" query:"limit" validate:"required,number,gte=1,lte=1000"` +} + +func (r *Paginate) Messages(_ *http.Request) map[string]string { + return map[string]string{ + "Page.gte": "页码必须大于或等于1", + "Limit.gte": "每页数量必须大于或等于1", + "Limit.lte": "每页数量必须小于或等于1000", + "Page.number": "页码必须是数字", + "Limit.number": "每页数量必须是数字", + "Page.required": "页码不能为空", + "Limit.required": "每页数量不能为空", + } +} + +func (r *Paginate) Prepare(_ *http.Request) error { + if r.Page == 0 { + r.Page = 1 + } + if r.Limit == 0 { + r.Limit = 10 + } + return nil +} diff --git a/internal/http/request/plugin.go b/internal/http/request/plugin.go new file mode 100644 index 00000000..e23f9cc4 --- /dev/null +++ b/internal/http/request/plugin.go @@ -0,0 +1,10 @@ +package request + +type PluginSlug struct { + Slug string `json:"slug" form:"slug"` +} + +type PluginUpdateShow struct { + Slug string `json:"slug" form:"slug"` + Show bool `json:"show" form:"show"` +} diff --git a/internal/http/request/request.go b/internal/http/request/request.go new file mode 100644 index 00000000..ffb7fc89 --- /dev/null +++ b/internal/http/request/request.go @@ -0,0 +1,21 @@ +package request + +import ( + "net/http" +) + +type WithAuthorize interface { + Authorize(r *http.Request) error +} + +type WithPrepare interface { + Prepare(r *http.Request) error +} + +type WithRules interface { + Rules(r *http.Request) map[string]string +} + +type WithMessages interface { + Messages(r *http.Request) map[string]string +} diff --git a/internal/http/request/safe.go b/internal/http/request/safe.go new file mode 100644 index 00000000..56f65090 --- /dev/null +++ b/internal/http/request/safe.go @@ -0,0 +1,10 @@ +package request + +type SafeUpdateSSH struct { + Port uint `json:"port" form:"port"` + Status bool `json:"status" form:"status"` +} + +type SafeUpdatePingStatus struct { + Status bool `json:"status" form:"status"` +} diff --git a/internal/http/request/setting.go b/internal/http/request/setting.go new file mode 100644 index 00000000..9575e6ba --- /dev/null +++ b/internal/http/request/setting.go @@ -0,0 +1,16 @@ +package request + +type PanelSetting struct { + Name string `json:"name"` + Language string `json:"language"` + Entrance string `json:"entrance"` + WebsitePath string `json:"website_path"` + BackupPath string `json:"backup_path"` + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Port string `json:"port"` + HTTPS bool `json:"https"` + Cert string `json:"cert"` + Key string `json:"key"` +} diff --git a/internal/http/request/ssh.go b/internal/http/request/ssh.go new file mode 100644 index 00000000..b49c8cbe --- /dev/null +++ b/internal/http/request/ssh.go @@ -0,0 +1,8 @@ +package request + +type SSHUpdateInfo struct { + Host string `json:"host" form:"host"` + Port string `json:"port" form:"port"` + User string `json:"user" form:"user"` + Password string `json:"password" form:"password"` +} diff --git a/internal/http/request/systemctl.go b/internal/http/request/systemctl.go new file mode 100644 index 00000000..bb3c2ee2 --- /dev/null +++ b/internal/http/request/systemctl.go @@ -0,0 +1,5 @@ +package request + +type SystemctlService struct { + Service string `json:"service"` +} diff --git a/internal/http/request/user.go b/internal/http/request/user.go new file mode 100644 index 00000000..401e010d --- /dev/null +++ b/internal/http/request/user.go @@ -0,0 +1,14 @@ +package request + +type UserLogin struct { + Username string `json:"username" form:"username" validate:"required,min=3,max=255"` + Password string `json:"password" form:"password" validate:"required,min=6,max=255"` +} + +type UserID struct { + ID uint `uri:"id" validate:"required,number"` +} + +type AddUser struct { + Name string `json:"name" form:"name" validate:"required,min=3,max=255" comment:"用户名"` +} diff --git a/internal/http/request/website.go b/internal/http/request/website.go new file mode 100644 index 00000000..d38a4bf2 --- /dev/null +++ b/internal/http/request/website.go @@ -0,0 +1,76 @@ +package request + +import "net/http" + +type WebsiteDefaultConfig struct { + Index string `json:"index" form:"index"` + Stop string `json:"stop" form:"stop"` +} + +type WebsiteCreate struct { + Name string `form:"name" json:"name"` + Domains []string `form:"domains" json:"domains"` + Ports []uint `form:"ports" json:"ports"` + Path string `form:"path" json:"path"` + PHP string `form:"php" json:"php"` + DB bool `form:"db" json:"db"` + DBType string `form:"db_type" json:"db_type"` + DBName string `form:"db_name" json:"db_name"` + DBUser string `form:"db_user" json:"db_user"` + DBPassword string `form:"db_password" json:"db_password"` +} + +type WebsiteDelete struct { + ID uint `form:"id" json:"id"` + Path bool `form:"path" json:"path"` + DB bool `form:"db" json:"db"` +} + +type WebsiteUpdate struct { + ID uint `form:"id" json:"id"` + Domains []string `form:"domains" json:"domains"` + Ports []uint `form:"ports" json:"ports"` + SSLPorts []uint `form:"ssl_ports" json:"ssl_ports"` + QUICPorts []uint `form:"quic_ports" json:"quic_ports"` + OCSP bool `form:"ocsp" json:"ocsp"` + HSTS bool `form:"hsts" json:"hsts"` + SSL bool `form:"ssl" json:"ssl"` + HTTPRedirect bool `form:"http_redirect" json:"http_redirect"` + OpenBasedir bool `form:"open_basedir" json:"open_basedir"` + Waf bool `form:"waf" json:"waf"` + WafCache string `form:"waf_cache" json:"waf_cache"` + WafMode string `form:"waf_mode" json:"waf_mode"` + WafCcDeny string `form:"waf_cc_deny" json:"waf_cc_deny"` + Index string `form:"index" json:"index"` + Path string `form:"path" json:"path"` + Root string `form:"root" json:"root"` + Raw string `form:"raw" json:"raw"` + Rewrite string `form:"rewrite" json:"rewrite"` + PHP int `form:"php" json:"php"` + SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"` + SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"` +} + +func (r *WebsiteUpdate) Prepare(_ *http.Request) error { + if r.WafMode == "" { + r.WafMode = "DYNAMIC" + } + if r.WafCcDeny == "" { + r.WafCcDeny = "rate=1000r/m duration=60m" + } + if r.WafCache == "" { + r.WafCache = "capacity=50" + } + + return nil +} + +type WebsiteUpdateRemark struct { + ID uint `form:"id" json:"id"` + Remark string `form:"remark" json:"remark"` +} + +type WebsiteUpdateStatus struct { + ID uint `json:"id" form:"id"` + Status bool `json:"status" form:"status"` +} diff --git a/internal/job/process_task.go b/internal/job/process_task.go new file mode 100644 index 00000000..380fe1ce --- /dev/null +++ b/internal/job/process_task.go @@ -0,0 +1,52 @@ +package job + +import ( + "errors" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/pkg/shell" +) + +// ProcessTask 处理面板任务 +type ProcessTask struct { + taskRepo biz.TaskRepo + taskID uint +} + +// NewProcessTask 实例化 ProcessTask +func NewProcessTask(taskRepo biz.TaskRepo) *ProcessTask { + return &ProcessTask{ + taskRepo: taskRepo, + } +} + +func (receiver *ProcessTask) Handle(args ...any) error { + taskID, ok := args[0].(uint) + if !ok { + return errors.New("参数错误") + } + receiver.taskID = taskID + + task, err := receiver.taskRepo.Get(taskID) + if err != nil { + return err + } + + if err = receiver.taskRepo.UpdateStatus(taskID, biz.TaskStatusRunning); err != nil { + return err + } + + if _, err = shell.Execf(task.Shell); err != nil { // nolint: govet + return err + } + + if err = receiver.taskRepo.UpdateStatus(taskID, biz.TaskStatusSuccess); err != nil { + return err + } + + return nil +} + +func (receiver *ProcessTask) ErrHandle(err error) { + _ = receiver.taskRepo.UpdateStatus(receiver.taskID, biz.TaskStatusFailed) +} diff --git a/internal/migration/migration.go b/internal/migration/migration.go new file mode 100644 index 00000000..466458e4 --- /dev/null +++ b/internal/migration/migration.go @@ -0,0 +1,5 @@ +package migration + +import "github.com/go-gormigrate/gormigrate/v2" + +var Migrations []*gormigrate.Migration diff --git a/internal/migration/v1.go b/internal/migration/v1.go new file mode 100644 index 00000000..8bcbdbc8 --- /dev/null +++ b/internal/migration/v1.go @@ -0,0 +1,44 @@ +package migration + +import ( + "github.com/go-gormigrate/gormigrate/v2" + "gorm.io/gorm" + + "github.com/TheTNB/panel/internal/biz" +) + +func init() { + Migrations = append(Migrations, &gormigrate.Migration{ + ID: "20240812-init", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate( + &biz.Cert{}, + &biz.CertDNS{}, + &biz.CertAccount{}, + &biz.Cron{}, + &biz.Database{}, + &biz.Monitor{}, + &biz.Plugin{}, + &biz.Setting{}, + &biz.Task{}, + &biz.User{}, + &biz.Website{}, + ) + }, + Rollback: func(tx *gorm.DB) error { + return tx.Migrator().DropTable( + &biz.Cert{}, + &biz.CertDNS{}, + &biz.CertAccount{}, + &biz.Cron{}, + &biz.Database{}, + &biz.Monitor{}, + &biz.Plugin{}, + &biz.Setting{}, + &biz.Task{}, + &biz.User{}, + &biz.Website{}, + ) + }, + }) +} diff --git a/internal/php.go b/internal/php.go deleted file mode 100644 index 7667dea7..00000000 --- a/internal/php.go +++ /dev/null @@ -1,25 +0,0 @@ -package internal - -import ( - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PHP interface { - Status() (bool, error) - Reload() error - Start() error - Stop() error - Restart() error - GetConfig() (string, error) - SaveConfig(config string) error - GetFPMConfig() (string, error) - SaveFPMConfig(config string) error - Load() ([]types.NV, error) - GetErrorLog() (string, error) - GetSlowLog() (string, error) - ClearErrorLog() error - ClearSlowLog() error - GetExtensions() ([]types.PHPExtension, error) - InstallExtension(slug string) error - UninstallExtension(slug string) error -} diff --git a/internal/plugin.go b/internal/plugin.go deleted file mode 100644 index 9170e9f4..00000000 --- a/internal/plugin.go +++ /dev/null @@ -1,16 +0,0 @@ -package internal - -import ( - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Plugin interface { - AllInstalled() ([]models.Plugin, error) - All() []*types.Plugin - GetBySlug(slug string) *types.Plugin - GetInstalledBySlug(slug string) models.Plugin - Install(slug string) error - Uninstall(slug string) error - Update(slug string) error -} diff --git a/internal/plugin/init.go b/internal/plugin/init.go new file mode 100644 index 00000000..f334855d --- /dev/null +++ b/internal/plugin/init.go @@ -0,0 +1,12 @@ +package plugin + +import ( + "github.com/go-chi/chi/v5" + + _ "github.com/TheTNB/panel/internal/plugin/openresty" + "github.com/TheTNB/panel/pkg/pluginloader" +) + +func Boot(r chi.Router) { + pluginloader.Boot(r) +} diff --git a/internal/plugin/openresty/init.go b/internal/plugin/openresty/init.go new file mode 100644 index 00000000..2de36be7 --- /dev/null +++ b/internal/plugin/openresty/init.go @@ -0,0 +1,22 @@ +package openresty + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/pluginloader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + pluginloader.Register(&types.Plugin{ + Slug: "openresty", + Name: "OpenResty", + Route: func(r chi.Router) { + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("Hello, World!")) + }) + }, + }) +} diff --git a/internal/plugin/openresty/service.go b/internal/plugin/openresty/service.go new file mode 100644 index 00000000..83ccc157 --- /dev/null +++ b/internal/plugin/openresty/service.go @@ -0,0 +1 @@ +package openresty diff --git a/internal/route/http.go b/internal/route/http.go new file mode 100644 index 00000000..6a94c34f --- /dev/null +++ b/internal/route/http.go @@ -0,0 +1,300 @@ +package route + +import ( + "io/fs" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + httpSwagger "github.com/swaggo/http-swagger/v2" + + _ "github.com/TheTNB/panel/docs" + "github.com/TheTNB/panel/internal/embed" + "github.com/TheTNB/panel/internal/http/middleware" + "github.com/TheTNB/panel/internal/service" +) + +func Http(r chi.Router) { + r.Route("/api", func(r chi.Router) { + r.Route("/user", func(r chi.Router) { + user := service.NewUserService() + r.With(middleware.Throttle(5, time.Minute)).Post("/login", user.Login) + r.Post("/logout", user.Logout) + r.Get("/isLogin", user.IsLogin) + r.With(middleware.MustLogin).Get("/info", user.Info) + }) + + r.Route("/info", func(r chi.Router) { + info := service.NewInfoService() + r.Get("/panel", info.Panel) + r.With(middleware.MustLogin).Get("/homePlugins", info.HomePlugins) + r.With(middleware.MustLogin).Get("/nowMonitor", info.NowMonitor) + r.With(middleware.MustLogin).Get("/systemInfo", info.SystemInfo) + r.With(middleware.MustLogin).Get("/countInfo", info.CountInfo) + r.With(middleware.MustLogin).Get("/installedDbAndPhp", info.InstalledDbAndPhp) + r.With(middleware.MustLogin).Get("/checkUpdate", info.CheckUpdate) + r.With(middleware.MustLogin).Get("/updateInfo", info.UpdateInfo) + r.With(middleware.MustLogin).Post("/update", info.Update) + r.With(middleware.MustLogin).Post("/restart", info.Restart) + }) + + r.Route("/task", func(r chi.Router) { + // TODO 修改前端 + r.Use(middleware.MustLogin) + task := service.NewTaskService() + r.Get("/status", task.Status) + r.Get("/", task.List) + r.Get("/{id}", task.Get) // TODO 修改前端 + r.Delete("/{id}", task.Delete) // TODO 修改前端 + }) + + r.Route("/website", func(r chi.Router) { + // TODO 修改前端 + r.Use(middleware.MustLogin) + // r.Use(middleware.MustInstallWebServer) + website := service.NewWebsiteService() + r.Get("/defaultConfig", website.GetDefaultConfig) + r.Post("/defaultConfig", website.UpdateDefaultConfig) + r.Get("/", website.List) + r.Post("/", website.Create) + r.Get("/{id}", website.Get) + r.Put("/{id}", website.Update) + r.Delete("/{id}", website.Delete) + r.Delete("/{id}/log", website.ClearLog) + r.Post("/{id}/updateRemark", website.UpdateRemark) + r.Post("/{id}/resetConfig", website.ResetConfig) + r.Post("/{id}/status", website.UpdateStatus) + }) + + // TODO + r.Route("/backup", func(r chi.Router) { + r.Use(middleware.MustLogin) + backup := service.NewBackupService() + r.Get("/backup", backup.List) + r.Post("/create", backup.Create) + r.Post("/update", backup.Update) + r.Get("/{id}", backup.Get) + r.Delete("/{id}", backup.Delete) + r.Delete("/{id}/restore", backup.Restore) + }) + + r.Route("/cert", func(r chi.Router) { + r.Use(middleware.MustLogin) + cert := service.NewCertService() + r.Get("/caProviders", cert.CAProviders) + r.Get("/dnsProviders", cert.DNSProviders) + r.Get("/algorithms", cert.Algorithms) + r.Route("/cert", func(r chi.Router) { + r.Get("/", cert.List) + r.Post("/", cert.Create) + r.Put("/{id}", cert.Update) + r.Get("/{id}", cert.Get) + r.Delete("/{id}", cert.Delete) + r.Post("/{id}/obtain", cert.Obtain) + r.Post("/{id}/renew", cert.Renew) + r.Post("/{id}/manualDNS", cert.ManualDNS) + r.Post("/{id}/deploy", cert.Deploy) + }) + r.Route("/dns", func(r chi.Router) { + certDNS := service.NewCertDNSService() + r.Get("/", certDNS.List) + r.Post("/", certDNS.Create) + r.Put("/{id}", certDNS.Update) + r.Get("/{id}", certDNS.Get) + r.Delete("/{id}", certDNS.Delete) + }) + r.Route("/account", func(r chi.Router) { + certAccount := service.NewCertAccountService() + r.Get("/", certAccount.List) + r.Post("/", certAccount.Create) + r.Put("/{id}", certAccount.Update) + r.Get("/{id}", certAccount.Get) + r.Delete("/{id}", certAccount.Delete) + }) + }) + + r.Route("/plugin", func(r chi.Router) { + r.Use(middleware.MustLogin) + plugin := service.NewPluginService() + r.Get("/list", plugin.List) + r.Post("/install", plugin.Install) + r.Post("/uninstall", plugin.Uninstall) + r.Post("/update", plugin.Update) + r.Post("/updateShow", plugin.UpdateShow) + r.Get("/isInstalled", plugin.IsInstalled) + }) + + r.Route("/cron", func(r chi.Router) { + r.Use(middleware.MustLogin) + cron := service.NewCronService() + r.Get("/", cron.List) + r.Post("/", cron.Create) + r.Put("/{id}", cron.Update) + r.Get("/{id}", cron.Get) + r.Delete("/{id}", cron.Delete) + r.Post("/{id}/status", cron.Status) + r.Get("/{id}/log", cron.Log) + }) + + r.Route("/safe", func(r chi.Router) { + r.Use(middleware.MustLogin) + safe := service.NewSafeService() + r.Get("/ssh", safe.GetSSH) + r.Post("/ssh", safe.UpdateSSH) + r.Get("/ping", safe.GetPingStatus) + r.Post("/ping", safe.UpdatePingStatus) + }) + + r.Route("/firewall", func(r chi.Router) { + r.Use(middleware.MustLogin) + firewall := service.NewFirewallService() + r.Get("/status", firewall.GetStatus) + r.Post("/status", firewall.UpdateStatus) + r.Get("/rule", firewall.GetRules) + r.Post("/rule", firewall.CreateRule) + r.Delete("/rule", firewall.DeleteRule) + }) + + r.Route("/ssh", func(r chi.Router) { + r.Use(middleware.MustLogin) + ssh := service.NewSSHService() + r.Get("/info", ssh.GetInfo) + r.Post("/info", ssh.UpdateInfo) + r.Get("/session", ssh.Session) + }) + + r.Route("/container", func(r chi.Router) { + r.Use(middleware.MustLogin) + r.Route("/container", func(r chi.Router) { + container := service.NewContainerService() + r.Get("/", container.List) + r.Get("/search", container.Search) + r.Post("/create", container.Create) + r.Post("/remove", container.Remove) + r.Post("/start", container.Start) + r.Post("/stop", container.Stop) + r.Post("/restart", container.Restart) + r.Post("/pause", container.Pause) + r.Post("/unpause", container.Unpause) + r.Get("/inspect", container.Inspect) + r.Post("/kill", container.Kill) + r.Post("/rename", container.Rename) + r.Get("/stats", container.Stats) + r.Get("/exist", container.Exist) + r.Get("/logs", container.Logs) + r.Post("/prune", container.Prune) + }) + r.Route("/network", func(r chi.Router) { + containerNetwork := service.NewContainerNetworkService() + r.Get("/list", containerNetwork.List) + r.Post("/create", containerNetwork.Create) + r.Post("/remove", containerNetwork.Remove) + r.Get("/exist", containerNetwork.Exist) + r.Get("/inspect", containerNetwork.Inspect) + r.Post("/connect", containerNetwork.Connect) + r.Post("/disconnect", containerNetwork.Disconnect) + r.Post("/prune", containerNetwork.Prune) + }) + r.Route("/image", func(r chi.Router) { + containerImage := service.NewContainerImageService() + r.Get("/list", containerImage.List) + r.Get("/exist", containerImage.Exist) + r.Post("/pull", containerImage.Pull) + r.Post("/remove", containerImage.Remove) + r.Get("/inspect", containerImage.Inspect) + r.Post("/prune", containerImage.Prune) + }) + r.Route("/volume", func(r chi.Router) { + containerVolume := service.NewContainerVolumeService() + r.Get("/list", containerVolume.List) + r.Post("/create", containerVolume.Create) + r.Get("/exist", containerVolume.Exist) + r.Post("/remove", containerVolume.Remove) + r.Get("/inspect", containerVolume.Inspect) + r.Post("/prune", containerVolume.Prune) + }) + }) + + r.Route("/file", func(r chi.Router) { + r.Use(middleware.MustLogin) + file := service.NewFileService() + r.Post("/create", file.Create) + r.Get("/content", file.Content) + r.Post("/save", file.Save) + r.Post("/delete", file.Delete) + r.Post("/upload", file.Upload) + r.Post("/move", file.Move) + r.Post("/copy", file.Copy) + r.Get("/download", file.Download) + r.Post("/remoteDownload", file.RemoteDownload) + r.Get("/info", file.Info) + r.Post("/permission", file.Permission) + r.Post("/compress", file.Compress) + r.Post("/unCompress", file.UnCompress) + r.Post("/search", file.Search) + r.Get("/list", file.List) + }) + + r.Route("/monitor", func(r chi.Router) { + r.Use(middleware.MustLogin) + monitor := service.NewMonitorService() + r.Get("/setting", monitor.GetSetting) + r.Post("/setting", monitor.UpdateSetting) + r.Post("/clear", monitor.Clear) + r.Get("/list", monitor.List) + }) + + r.Route("/setting", func(r chi.Router) { + r.Use(middleware.MustLogin) + setting := service.NewSettingService() + r.Get("/", setting.Get) + r.Post("/", setting.Update) + }) + + r.Route("/systemctl", func(r chi.Router) { + r.Use(middleware.MustLogin) + systemctl := service.NewSystemctlService() + r.Get("/status", systemctl.Status) + r.Get("/isEnabled", systemctl.IsEnabled) + r.Post("/enable", systemctl.Enable) + r.Post("/disable", systemctl.Disable) + r.Post("/restart", systemctl.Restart) + r.Post("/reload", systemctl.Reload) + r.Post("/start", systemctl.Start) + r.Post("/stop", systemctl.Stop) + }) + + }) + + r.With(middleware.MustLogin).Mount("/swagger", httpSwagger.Handler()) + r.NotFound(func(writer http.ResponseWriter, request *http.Request) { + frontend, _ := fs.Sub(embed.PublicFS, "frontend") + spaHandler := func(fs http.FileSystem) http.HandlerFunc { + fileServer := http.FileServer(fs) + return func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + f, err := fs.Open(path) + if err != nil { + indexFile, err := fs.Open("index.html") + if err != nil { + http.NotFound(w, r) + return + } + defer indexFile.Close() + + fi, err := indexFile.Stat() + if err != nil { + http.NotFound(w, r) + return + } + + http.ServeContent(w, r, "index.html", fi.ModTime(), indexFile) + return + } + defer f.Close() + fileServer.ServeHTTP(w, r) + } + } + spaHandler(http.FS(frontend)).ServeHTTP(writer, request) + }) +} diff --git a/internal/service/backup.go b/internal/service/backup.go new file mode 100644 index 00000000..2f27ff27 --- /dev/null +++ b/internal/service/backup.go @@ -0,0 +1,34 @@ +package service + +import "net/http" + +type BackupService struct { +} + +func NewBackupService() *BackupService { + return &BackupService{} +} + +func (s *BackupService) List(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Create(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Update(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Get(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Delete(w http.ResponseWriter, r *http.Request) { + +} + +func (s *BackupService) Restore(w http.ResponseWriter, r *http.Request) { + +} diff --git a/internal/service/base.go b/internal/service/base.go new file mode 100644 index 00000000..0409ee03 --- /dev/null +++ b/internal/service/base.go @@ -0,0 +1,138 @@ +package service + +import ( + "errors" + "fmt" + "net/http" + "slices" + "strings" + + "github.com/go-playground/validator/v10" + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/http/request" +) + +// SuccessResponse 通用成功响应 +type SuccessResponse struct { + Message string `json:"message"` + Data any `json:"data"` +} + +// ErrorResponse 通用错误响应 +type ErrorResponse struct { + Message string `json:"message"` +} + +// Success 响应成功 +func Success(w http.ResponseWriter, data any) { + render := chix.NewRender(w) + defer render.Release() + render.JSON(&SuccessResponse{ + Message: "success", + Data: data, + }) +} + +// Error 响应错误 +func Error(w http.ResponseWriter, code int, message string) { + render := chix.NewRender(w) + defer render.Release() + render.Status(code) + render.JSON(&ErrorResponse{ + Message: message, + }) +} + +// ErrorSystem 响应系统错误 +func ErrorSystem(w http.ResponseWriter) { + render := chix.NewRender(w) + defer render.Release() + render.Status(http.StatusInternalServerError) + render.JSON(&ErrorResponse{ + Message: "系统内部错误", + }) +} + +// Bind 验证并绑定请求参数 +func Bind[T any](r *http.Request) (*T, error) { + req := new(T) + + // 绑定参数 + binder := chix.NewBind(r) + defer binder.Release() + if err := binder.URI(req); err != nil { + return nil, err + } + if err := binder.Query(req); err != nil { + return nil, err + } + if slices.Contains([]string{"POST", "PUT", "PATCH"}, strings.ToUpper(r.Method)) { + if err := binder.Body(req); err != nil { + return nil, err + } + } + + // 准备验证 + if reqWithPrepare, ok := any(req).(request.WithPrepare); ok { + if err := reqWithPrepare.Prepare(r); err != nil { + return nil, err + } + } + if reqWithAuthorize, ok := any(req).(request.WithAuthorize); ok { + if err := reqWithAuthorize.Authorize(r); err != nil { + return nil, err + } + } + if reqWithRules, ok := any(req).(request.WithRules); ok { + if rules := reqWithRules.Rules(r); rules != nil { + app.Validator.RegisterStructValidationMapRules(rules, req) + } + } + + // 验证参数 + err := app.Validator.Struct(req) + if err == nil { + return req, nil + } + + // 翻译错误信息 + var errs validator.ValidationErrors + if errors.As(err, &errs) { + for _, e := range errs { + if reqWithMessages, ok := any(req).(request.WithMessages); ok { + if msg, found := reqWithMessages.Messages(r)[fmt.Sprintf("%s.%s", e.Field(), e.Tag())]; found { + return nil, errors.New(msg) + } + } + return nil, errors.New(e.Translate(*app.Translator)) // nolint:staticcheck + } + } + + return nil, err +} + +// Paginate 取分页条目 +func Paginate[T any](r *http.Request, allItems []T) (pagedItems []T, total uint) { + req, err := Bind[request.Paginate](r) + if err != nil { + req.Page = 1 + req.Limit = 10 + } + total = uint(len(allItems)) + startIndex := (req.Page - 1) * req.Limit + endIndex := req.Page * req.Limit + + if total == 0 { + return []T{}, 0 + } + if startIndex > total { + return []T{}, total + } + if endIndex > total { + endIndex = total + } + + return allItems[startIndex:endIndex], total +} diff --git a/internal/service/cert.go b/internal/service/cert.go new file mode 100644 index 00000000..9df4c330 --- /dev/null +++ b/internal/service/cert.go @@ -0,0 +1,341 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/acme" +) + +type CertService struct { + certRepo biz.CertRepo +} + +func NewCertService() *CertService { + return &CertService{ + certRepo: data.NewCertRepo(), + } +} + +// CAProviders +// +// @Summary 获取 CA 提供商 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/caProviders [get] +func (s *CertService) CAProviders(w http.ResponseWriter, r *http.Request) { + Success(w, []map[string]string{ + { + "name": "Let's Encrypt", + "ca": "letsencrypt", + }, + { + "name": "ZeroSSL", + "ca": "zerossl", + }, + { + "name": "SSL.com", + "ca": "sslcom", + }, + { + "name": "Google", + "ca": "google", + }, + { + "name": "Buypass", + "ca": "buypass", + }, + }) + +} + +// DNSProviders +// +// @Summary 获取 DNS 提供商 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/dnsProviders [get] +func (s *CertService) DNSProviders(w http.ResponseWriter, r *http.Request) { + Success(w, []map[string]any{ + { + "name": "DNSPod", + "dns": acme.DnsPod, + }, + { + "name": "腾讯云", + "dns": acme.Tencent, + }, + { + "name": "阿里云", + "dns": acme.AliYun, + }, + { + "name": "CloudFlare", + "dns": acme.CloudFlare, + }, + }) + +} + +// Algorithms +// +// @Summary 获取算法列表 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/algorithms [get] +func (s *CertService) Algorithms(w http.ResponseWriter, r *http.Request) { + Success(w, []map[string]any{ + { + "name": "EC256", + "key": acme.KeyEC256, + }, + { + "name": "EC384", + "key": acme.KeyEC384, + }, + { + "name": "RSA2048", + "key": acme.KeyRSA2048, + }, + { + "name": "RSA4096", + "key": acme.KeyRSA4096, + }, + }) + +} + +// List +// +// @Summary 证书列表 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/cert [get] +func (s *CertService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certs, total, err := s.certRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": certs, + }) +} + +// Create +// +// @Summary 创建证书 +// @Tags 证书服务 +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /cert/cert [post] +func (s *CertService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cert, err := s.certRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, cert) +} + +// Update +// +// @Summary 更新证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/cert/{id} [post] +func (s *CertService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Get +// +// @Summary 获取证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/cert/{id} [get] +func (s *CertService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cert, err := s.certRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, cert) +} + +// Delete +// +// @Summary 删除证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/cert/{id} [delete] +func (s *CertService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + err = s.certRepo.Delete(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Obtain +// +// @Summary 签发证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/{id}/obtain [post] +func (s *CertService) Obtain(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cert, err := s.certRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if cert.DNS != nil || cert.Website != nil { + _, err = s.certRepo.ObtainAuto(req.ID) + } else { + _, err = s.certRepo.ObtainManual(req.ID) + } + + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Renew +// +// @Summary 续签证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/{id}/renew [post] +func (s *CertService) Renew(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + _, err = s.certRepo.Renew(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// ManualDNS +// +// @Summary 手动 DNS +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/{id}/manualDNS [post] +func (s *CertService) ManualDNS(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + dns, err := s.certRepo.ManualDNS(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, dns) +} + +// Deploy +// +// @Summary 部署证书 +// @Tags 证书服务 +// @Produce json +// @Param id path int true "证书 ID" +// @Param websiteID query int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /cert/{id}/deploy [post] +func (s *CertService) Deploy(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertDeploy](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + err = s.certRepo.Deploy(req.ID, req.WebsiteID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/cert_account.go b/internal/service/cert_account.go new file mode 100644 index 00000000..29419578 --- /dev/null +++ b/internal/service/cert_account.go @@ -0,0 +1,102 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type CertAccountService struct { + certAccountRepo biz.CertAccountRepo +} + +func NewCertAccountService() *CertAccountService { + return &CertAccountService{ + certAccountRepo: data.NewCertAccountRepo(), + } +} + +func (s *CertAccountService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certDNS, total, err := s.certAccountRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": certDNS, + }) +} + +func (s *CertAccountService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertAccountCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + account, err := s.certAccountRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, account) +} + +func (s *CertAccountService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertAccountUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certAccountRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CertAccountService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + account, err := s.certAccountRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, account) +} + +func (s *CertAccountService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certAccountRepo.Delete(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/cert_dns.go b/internal/service/cert_dns.go new file mode 100644 index 00000000..e6306c68 --- /dev/null +++ b/internal/service/cert_dns.go @@ -0,0 +1,102 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type CertDNSService struct { + certDNSRepo biz.CertDNSRepo +} + +func NewCertDNSService() *CertDNSService { + return &CertDNSService{ + certDNSRepo: data.NewCertDNSRepo(), + } +} + +func (s *CertDNSService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certDNS, total, err := s.certDNSRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": certDNS, + }) +} + +func (s *CertDNSService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertDNSCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certDNS, err := s.certDNSRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, certDNS) +} + +func (s *CertDNSService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CertDNSUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certDNSRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CertDNSService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + certDNS, err := s.certDNSRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, certDNS) +} + +func (s *CertDNSService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.certDNSRepo.Delete(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/container.go b/internal/service/container.go new file mode 100644 index 00000000..1653ba6d --- /dev/null +++ b/internal/service/container.go @@ -0,0 +1,279 @@ +package service + +import ( + "net/http" + "strings" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerService struct { + containerRepo biz.ContainerRepo +} + +func NewContainerService() *ContainerService { + return &ContainerService{ + containerRepo: data.NewContainerRepo(), + } +} + +func (s *ContainerService) List(w http.ResponseWriter, r *http.Request) { + containers, err := s.containerRepo.ListAll() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + } + + paged, total := Paginate(r, containers) + items := make([]any, len(paged)) + for _, item := range paged { + var name string + if len(item.Names) > 0 { + name = item.Names[0] + } + items = append(items, map[string]any{ + "id": item.ID, + "name": strings.TrimLeft(name, "/"), + "image": item.Image, + "image_id": item.ImageID, + "command": item.Command, + "created": carbon.CreateFromTimestamp(item.Created).ToDateTimeString(), + "ports": item.Ports, + "labels": item.Labels, + "state": item.State, + "status": item.Status, + }) + } + + Success(w, chix.M{ + "total": total, + "items": items, + }) +} + +func (s *ContainerService) Search(w http.ResponseWriter, r *http.Request) { + name := strings.Fields(r.FormValue("name")) + containers, err := s.containerRepo.ListByNames(name) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": len(containers), + "items": containers, + }) +} + +func (s *ContainerService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + id, err := s.containerRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, id) +} + +func (s *ContainerService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Remove(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Start(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Start(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Stop(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Stop(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Restart(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Restart(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Pause(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Pause(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Unpause(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Unpause(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Inspect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + container, err := s.containerRepo.Inspect(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, container) +} + +func (s *ContainerService) Kill(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Kill(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Rename(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerRename](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerRepo.Rename(req.ID, req.Name); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerService) Stats(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + stats, err := s.containerRepo.Stats(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, stats) +} + +func (s *ContainerService) Exist(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + exist, err := s.containerRepo.Exist(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, exist) +} + +func (s *ContainerService) Logs(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + logs, err := s.containerRepo.Logs(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, logs) +} + +func (s *ContainerService) Prune(w http.ResponseWriter, r *http.Request) { + if err := s.containerRepo.Prune(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/container_image.go b/internal/service/container_image.go new file mode 100644 index 00000000..2cba1daf --- /dev/null +++ b/internal/service/container_image.go @@ -0,0 +1,122 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/str" +) + +type ContainerImageService struct { + containerImageRepo biz.ContainerImageRepo +} + +func NewContainerImageService() *ContainerImageService { + return &ContainerImageService{ + containerImageRepo: data.NewContainerImageRepo(), + } +} + +func (s *ContainerImageService) List(w http.ResponseWriter, r *http.Request) { + images, err := s.containerImageRepo.List() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paged, total := Paginate(r, images) + + items := make([]any, len(paged)) + for _, item := range paged { + items = append(items, map[string]any{ + "id": item.ID, + "created": carbon.CreateFromTimestamp(item.Created).ToDateTimeString(), + "containers": item.Containers, + "size": str.FormatBytes(float64(item.Size)), + "labels": item.Labels, + "repo_tags": item.RepoTags, + "repo_digests": item.RepoDigests, + }) + } + + Success(w, chix.M{ + "total": total, + "items": items, + }) +} + +func (s *ContainerImageService) Exist(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerImageID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + exist, err := s.containerImageRepo.Exist(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, exist) +} + +func (s *ContainerImageService) Pull(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerImagePull](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerImageRepo.Pull(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerImageService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerImageID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerImageRepo.Remove(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerImageService) Inspect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerImageID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + inspect, err := s.containerImageRepo.Inspect(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, inspect) +} + +func (s *ContainerImageService) Prune(w http.ResponseWriter, r *http.Request) { + if err := s.containerImageRepo.Prune(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/container_network.go b/internal/service/container_network.go new file mode 100644 index 00000000..c5b7d286 --- /dev/null +++ b/internal/service/container_network.go @@ -0,0 +1,170 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type ContainerNetworkService struct { + containerNetworkRepo biz.ContainerNetworkRepo +} + +func NewContainerNetworkService() *ContainerNetworkService { + return &ContainerNetworkService{ + containerNetworkRepo: data.NewContainerNetworkRepo(), + } +} + +func (s *ContainerNetworkService) List(w http.ResponseWriter, r *http.Request) { + networks, err := s.containerNetworkRepo.List() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paged, total := Paginate(r, networks) + + items := make([]any, len(paged)) + for _, item := range paged { + var ipamConfig []any + for _, v := range item.IPAM.Config { + ipamConfig = append(ipamConfig, map[string]any{ + "subnet": v.Subnet, + "gateway": v.Gateway, + "ip_range": v.IPRange, + "aux_address": v.AuxAddress, + }) + } + items = append(items, map[string]any{ + "id": item.ID, + "name": item.Name, + "driver": item.Driver, + "ipv6": item.EnableIPv6, + "scope": item.Scope, + "internal": item.Internal, + "attachable": item.Attachable, + "ingress": item.Ingress, + "labels": item.Labels, + "options": item.Options, + "ipam": map[string]any{ + "config": ipamConfig, + "driver": item.IPAM.Driver, + "options": item.IPAM.Options, + }, + "created": carbon.CreateFromStdTime(item.Created).ToDateTimeString(), + }) + } + + Success(w, chix.M{ + "total": total, + "items": items, + }) +} + +func (s *ContainerNetworkService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + id, err := s.containerNetworkRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, id) +} + +func (s *ContainerNetworkService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerNetworkRepo.Remove(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerNetworkService) Exist(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + exist, err := s.containerNetworkRepo.Exist(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, exist) +} + +func (s *ContainerNetworkService) Inspect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + network, err := s.containerNetworkRepo.Inspect(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, network) +} + +func (s *ContainerNetworkService) Connect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkConnect](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerNetworkRepo.Connect(req.Network, req.Container); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerNetworkService) Disconnect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerNetworkConnect](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerNetworkRepo.Disconnect(req.Network, req.Container); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerNetworkService) Prune(w http.ResponseWriter, r *http.Request) { + if err := s.containerNetworkRepo.Prune(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/container_volume.go b/internal/service/container_volume.go new file mode 100644 index 00000000..6bbe8704 --- /dev/null +++ b/internal/service/container_volume.go @@ -0,0 +1,133 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/str" +) + +type ContainerVolumeService struct { + containerVolumeRepo biz.ContainerVolumeRepo +} + +func NewContainerVolumeService() *ContainerVolumeService { + return &ContainerVolumeService{ + containerVolumeRepo: data.NewContainerVolumeRepo(), + } +} + +func (s *ContainerVolumeService) List(w http.ResponseWriter, r *http.Request) { + volumes, err := s.containerVolumeRepo.List() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paged, total := Paginate(r, volumes) + + items := make([]any, len(paged)) + for _, item := range paged { + var usage any + if item.UsageData != nil { + usage = map[string]any{ + "ref_count": item.UsageData.RefCount, + "size": str.FormatBytes(float64(item.UsageData.Size)), + } + } + items = append(items, map[string]any{ + "id": item.Name, + "created": carbon.Parse(item.CreatedAt).ToDateTimeString(), + "driver": item.Driver, + "mount": item.Mountpoint, + "labels": item.Labels, + "options": item.Options, + "scope": item.Scope, + "status": item.Status, + "usage": usage, + }) + } + + Success(w, chix.M{ + "total": total, + "items": items, + }) +} + +func (s *ContainerVolumeService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerVolumeCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + volume, err := s.containerVolumeRepo.Create(req) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, volume.Name) + +} + +func (s *ContainerVolumeService) Exist(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerVolumeID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + exist, err := s.containerVolumeRepo.Exist(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, exist) +} + +func (s *ContainerVolumeService) Remove(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerVolumeID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.containerVolumeRepo.Remove(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *ContainerVolumeService) Inspect(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ContainerVolumeID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + volume, err := s.containerVolumeRepo.Inspect(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, volume) +} + +func (s *ContainerVolumeService) Prune(w http.ResponseWriter, r *http.Request) { + if err := s.containerVolumeRepo.Prune(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/cron.go b/internal/service/cron.go new file mode 100644 index 00000000..67b6b1f7 --- /dev/null +++ b/internal/service/cron.go @@ -0,0 +1,132 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type CronService struct { + cronRepo biz.CronRepo +} + +func NewCronService() *CronService { + return &CronService{ + cronRepo: data.NewCronRepo(), + } +} + +func (s *CronService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cron, total, err := s.cronRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": cron, + }) +} + +func (s *CronService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CronCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.cronRepo.Create(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CronService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CronUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.cronRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CronService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + cron, err := s.cronRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, cron) +} + +func (s *CronService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.cronRepo.Delete(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CronService) Status(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.CronStatus](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.cronRepo.Status(req.ID, req.Status); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *CronService) Log(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + log, err := s.cronRepo.Log(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, log) +} diff --git a/internal/service/file.go b/internal/service/file.go new file mode 100644 index 00000000..10fef5d0 --- /dev/null +++ b/internal/service/file.go @@ -0,0 +1,355 @@ +//go:build linux + +package service + +import ( + "fmt" + "net/http" + stdos "os" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/os" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" +) + +type FileService struct { +} + +func NewFileService() *FileService { + return &FileService{} +} + +func (s *FileService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCreate](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if !req.Dir { + if out, err := shell.Execf("touch %s", req.Path); err != nil { + Error(w, http.StatusInternalServerError, out) + return + } + } else { + if err = io.Mkdir(req.Path, 0755); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Content(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfo, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if fileInfo.IsDir() { + Error(w, http.StatusInternalServerError, "目标路径不是文件") + return + } + if fileInfo.Size() > 10*1024*1024 { + Error(w, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑") + return + } + + content, err := io.Read(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, content) +} + +func (s *FileService) Save(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileSave](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfo, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Write(req.Path, req.Content, fileInfo.Mode()); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Remove(req.Path); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileUpload](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Write(req.Path, string(req.File), 0755); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Move(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileMove](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if io.Exists(req.Target) && !req.Force { + Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在") + } + + if err = io.Mv(req.Source, req.Target); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCopy](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if io.Exists(req.Target) && !req.Force { + Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在") + } + + if err = io.Cp(req.Source, req.Target); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Download(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + info, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if info.IsDir() { + Error(w, http.StatusInternalServerError, "不能下载目录") + return + } + + render := chix.NewRender(w, r) + defer render.Release() + render.Download(req.Path, info.Name()) +} + +func (s *FileService) RemoteDownload(w http.ResponseWriter, r *http.Request) { + // TODO: 未实现 +} + +func (s *FileService) Info(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + info, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "name": info.Name(), + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "dir": info.IsDir(), + "modify": carbon.CreateFromStdTime(info.ModTime()).ToDateTimeString(), + }) +} + +func (s *FileService) Permission(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePermission](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + // 解析成8进制 + mode, err := strconv.ParseUint(req.Mode, 8, 64) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Chmod(req.Path, stdos.FileMode(mode)); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if err = io.Chown(req.Path, req.Owner, req.Group); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Compress(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCompress](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Compress(req.Paths, req.File, io.Zip); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.File, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileUnCompress](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.UnCompress(req.File, req.Path, io.Zip); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Search(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileSearch](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paths := make(map[string]stdos.FileInfo) + err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error { + if err != nil { + return err + } + if strings.Contains(info.Name(), req.KeyWord) { + paths[path] = info + } + return nil + }) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, paths) +} + +func (s *FileService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfoList, err := io.ReadDir(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + var paths []any + for _, fileInfo := range fileInfoList { + info, _ := fileInfo.Info() + stat := info.Sys().(*syscall.Stat_t) + + paths = append(paths, map[string]any{ + "name": info.Name(), + "full": filepath.Join(req.Path, info.Name()), + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "owner": os.GetUser(stat.Uid), + "group": os.GetGroup(stat.Gid), + "uid": stat.Uid, + "gid": stat.Gid, + "hidden": io.IsHidden(info.Name()), + "symlink": io.IsSymlink(info.Mode()), + "link": io.GetSymlink(filepath.Join(req.Path, info.Name())), + "dir": info.IsDir(), + "modify": carbon.CreateFromStdTime(info.ModTime()).ToDateTimeString(), + }) + } + + paged, total := Paginate(r, paths) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +// setPermission +func (s *FileService) setPermission(path string, mode stdos.FileMode, owner, group string) { + _ = io.Chmod(path, mode) + _ = io.Chown(path, owner, group) +} diff --git a/internal/service/file_windows.go b/internal/service/file_windows.go new file mode 100644 index 00000000..91744258 --- /dev/null +++ b/internal/service/file_windows.go @@ -0,0 +1,352 @@ +//go:build !linux + +package service + +import ( + "fmt" + "net/http" + stdos "os" + "path/filepath" + "strconv" + "strings" + + "github.com/go-rat/chix" + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" +) + +type FileService struct { +} + +func NewFileService() *FileService { + return &FileService{} +} + +func (s *FileService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCreate](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if !req.Dir { + if out, err := shell.Execf("touch %s", req.Path); err != nil { + Error(w, http.StatusInternalServerError, out) + return + } + } else { + if err = io.Mkdir(req.Path, 0755); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Content(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfo, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if fileInfo.IsDir() { + Error(w, http.StatusInternalServerError, "目标路径不是文件") + return + } + if fileInfo.Size() > 10*1024*1024 { + Error(w, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑") + return + } + + content, err := io.Read(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, content) +} + +func (s *FileService) Save(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileSave](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfo, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Write(req.Path, req.Content, fileInfo.Mode()); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Remove(req.Path); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileUpload](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Write(req.Path, string(req.File), 0755); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Move(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileMove](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if io.Exists(req.Target) && !req.Force { + Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在") + } + + if err = io.Mv(req.Source, req.Target); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCopy](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if io.Exists(req.Target) && !req.Force { + Error(w, http.StatusForbidden, "目标路径"+req.Target+"已存在") + } + + if err = io.Cp(req.Source, req.Target); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Download(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + info, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if info.IsDir() { + Error(w, http.StatusInternalServerError, "不能下载目录") + return + } + + render := chix.NewRender(w, r) + defer render.Release() + render.Download(req.Path, info.Name()) +} + +func (s *FileService) RemoteDownload(w http.ResponseWriter, r *http.Request) { + // TODO: 未实现 +} + +func (s *FileService) Info(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + info, err := io.FileInfo(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "name": info.Name(), + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "dir": info.IsDir(), + "modify": carbon.CreateFromStdTime(info.ModTime()).ToDateTimeString(), + }) +} + +func (s *FileService) Permission(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePermission](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + // 解析成8进制 + mode, err := strconv.ParseUint(req.Mode, 8, 64) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Chmod(req.Path, stdos.FileMode(mode)); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + if err = io.Chown(req.Path, req.Owner, req.Group); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FileService) Compress(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileCompress](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.Compress(req.Paths, req.File, io.Zip); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.File, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileUnCompress](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + if err = io.UnCompress(req.File, req.Path, io.Zip); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + s.setPermission(req.Path, 0755, "www", "www") + Success(w, nil) +} + +func (s *FileService) Search(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FileSearch](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paths := make(map[string]stdos.FileInfo) + err = filepath.Walk(req.Path, func(path string, info stdos.FileInfo, err error) error { + if err != nil { + return err + } + if strings.Contains(info.Name(), req.KeyWord) { + paths[path] = info + } + return nil + }) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, paths) +} + +func (s *FileService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FilePath](r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + fileInfoList, err := io.ReadDir(req.Path) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + var paths []any + for _, fileInfo := range fileInfoList { + info, _ := fileInfo.Info() + + paths = append(paths, map[string]any{ + "name": info.Name(), + "full": filepath.Join(req.Path, info.Name()), + "size": str.FormatBytes(float64(info.Size())), + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "owner": "", + "group": "", + "uid": 0, + "gid": 0, + "hidden": io.IsHidden(info.Name()), + "symlink": io.IsSymlink(info.Mode()), + "link": io.GetSymlink(filepath.Join(req.Path, info.Name())), + "dir": info.IsDir(), + "modify": carbon.CreateFromStdTime(info.ModTime()).ToDateTimeString(), + }) + } + + paged, total := Paginate(r, paths) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +// setPermission +func (s *FileService) setPermission(path string, mode stdos.FileMode, owner, group string) { + _ = io.Chmod(path, mode) + _ = io.Chown(path, owner, group) +} diff --git a/internal/service/firewall.go b/internal/service/firewall.go new file mode 100644 index 00000000..b4f6a925 --- /dev/null +++ b/internal/service/firewall.go @@ -0,0 +1,104 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/firewall" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type FirewallService struct { + firewall *firewall.Firewall +} + +func NewFirewallService() *FirewallService { + + return &FirewallService{ + firewall: firewall.NewFirewall(), + } +} + +func (s *FirewallService) GetStatus(w http.ResponseWriter, r *http.Request) { + running, err := s.firewall.Status() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, running) +} + +func (s *FirewallService) UpdateStatus(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallStatus](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if req.Status { + err = systemctl.Start("firewalld") + if err == nil { + err = systemctl.Enable("firewalld") + } + } else { + err = systemctl.Stop("firewalld") + if err == nil { + err = systemctl.Disable("firewalld") + } + } + + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FirewallService) GetRules(w http.ResponseWriter, r *http.Request) { + rules, err := s.firewall.ListRule() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + paged, total := Paginate(r, rules) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +func (s *FirewallService) CreateRule(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallCreateRule](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.firewall.Port(firewall.FireInfo{Port: req.Port, Protocol: req.Protocol}, "add"); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *FirewallService) DeleteRule(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallCreateRule](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.firewall.Port(firewall.FireInfo{Port: req.Port, Protocol: req.Protocol}, "remove"); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/info.go b/internal/service/info.go new file mode 100644 index 00000000..0f665c88 --- /dev/null +++ b/internal/service/info.go @@ -0,0 +1,387 @@ +package service + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/go-rat/chix" + "github.com/hashicorp/go-version" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/pkg/db" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/tools" + "github.com/TheTNB/panel/pkg/types" +) + +type InfoService struct { + taskRepo biz.TaskRepo + websiteRepo biz.WebsiteRepo + pluginRepo biz.PluginRepo + settingRepo biz.SettingRepo + cronRepo biz.CronRepo +} + +func NewInfoService() *InfoService { + return &InfoService{ + taskRepo: data.NewTaskRepo(), + websiteRepo: data.NewWebsiteRepo(), + pluginRepo: data.NewPluginRepo(), + settingRepo: data.NewSettingRepo(), + cronRepo: data.NewCronRepo(), + } +} + +// Panel +// +// @Summary 面板信息 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/panel [get] +func (s *InfoService) Panel(w http.ResponseWriter, r *http.Request) { + name, _ := s.settingRepo.Get(biz.SettingKeyName) + if name == "" { + name = "耗子面板" + } + + Success(w, chix.M{ + "name": name, + "language": app.Conf.MustString("app.locale"), + }) +} + +// HomePlugins +// +// @Summary 首页插件 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/homePlugins [get] +func (s *InfoService) HomePlugins(w http.ResponseWriter, r *http.Request) { + Success(w, nil) +} + +// NowMonitor +// +// @Summary 实时监控 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/nowMonitor [get] +func (s *InfoService) NowMonitor(w http.ResponseWriter, r *http.Request) { + Success(w, tools.GetMonitoringInfo()) +} + +// SystemInfo +// +// @Summary 系统信息 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/systemInfo [get] +func (s *InfoService) SystemInfo(w http.ResponseWriter, r *http.Request) { + monitorInfo := tools.GetMonitoringInfo() + + Success(w, chix.M{ + "os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion, + "uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400), + "panel_version": app.Conf.MustString("app.version"), + }) +} + +// CountInfo +// +// @Summary 统计信息 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/countInfo [get] +func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) { + websiteCount, err := s.websiteRepo.Count() + if err != nil { + Error(w, http.StatusInternalServerError, "获取网站数量失败") + return + } + + mysqlInstalled, _ := s.pluginRepo.IsInstalled("slug like ?", "mysql%") + postgresqlInstalled, _ := s.pluginRepo.IsInstalled("slug like ?", "postgresql%") + + type database struct { + Name string `json:"name"` + } + var databaseCount int64 + if mysqlInstalled { + rootPassword, _ := s.settingRepo.Get(biz.SettingKeyMysqlRootPassword) + mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock") + if err == nil { + defer mysql.Close() + if err = mysql.Ping(); err != nil { + databaseCount = -1 + } else { + rows, err := mysql.Query("SHOW DATABASES") + if err != nil { + databaseCount = -1 + } else { + defer rows.Close() + var databases []database + for rows.Next() { + var d database + if err := rows.Scan(&d.Name); err != nil { + continue + } + if d.Name == "information_schema" || d.Name == "performance_schema" || d.Name == "mysql" || d.Name == "sys" { + continue + } + + databases = append(databases, d) + } + databaseCount = int64(len(databases)) + } + } + } + } + if postgresqlInstalled { + postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432) + if err == nil { + defer postgres.Close() + if err = postgres.Ping(); err != nil { + databaseCount = -1 + } else { + rows, err := postgres.Query("SELECT datname FROM pg_database WHERE datistemplate = false") + if err != nil { + databaseCount = -1 + } else { + defer rows.Close() + var databases []database + for rows.Next() { + var d database + if err = rows.Scan(&d.Name); err != nil { + continue + } + if d.Name == "postgres" || d.Name == "template0" || d.Name == "template1" { + continue + } + databases = append(databases, d) + } + databaseCount = int64(len(databases)) + } + } + } + } + + var ftpCount int64 + ftpInstalled, _ := s.pluginRepo.IsInstalled("slug = ?", "pureftpd") + if ftpInstalled { + listRaw, err := shell.Execf("pure-pw list") + if len(listRaw) != 0 && err == nil { + listArr := strings.Split(listRaw, "\n") + ftpCount = int64(len(listArr)) + } + } + + cronCount, err := s.cronRepo.Count() + if err != nil { + cronCount = -1 + } + + Success(w, chix.M{ + "website": websiteCount, + "database": databaseCount, + "ftp": ftpCount, + "cron": cronCount, + }) +} + +// InstalledDbAndPhp +// +// @Summary 已安装的数据库和PHP +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/installedDbAndPhp [get] +func (s *InfoService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request) { + mysqlInstalled, _ := s.pluginRepo.IsInstalled("slug like ?", "mysql%") + postgresqlInstalled, _ := s.pluginRepo.IsInstalled("slug like ?", "postgresql%") + php, _ := s.pluginRepo.GetInstalledAll("slug like ?", "php%") + + var phpData []types.LV + var dbData []types.LV + phpData = append(phpData, types.LV{Value: "0", Label: "不使用"}) + dbData = append(dbData, types.LV{Value: "0", Label: "不使用"}) + for _, p := range php { + // 过滤 phpmyadmin + match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug) + if len(match) == 0 { + continue + } + + plugin, _ := s.pluginRepo.Get(p.Slug) + phpData = append(phpData, types.LV{Value: strings.ReplaceAll(p.Slug, "php", ""), Label: plugin.Name}) + } + + if mysqlInstalled { + dbData = append(dbData, types.LV{Value: "mysql", Label: "MySQL"}) + } + if postgresqlInstalled { + dbData = append(dbData, types.LV{Value: "postgresql", Label: "PostgreSQL"}) + } + + Success(w, chix.M{ + "php": phpData, + "db": dbData, + }) +} + +// CheckUpdate +// +// @Summary 检查更新 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/checkUpdate [get] +func (s *InfoService) CheckUpdate(w http.ResponseWriter, r *http.Request) { + current := app.Conf.MustString("app.version") + latest, err := tools.GetLatestPanelVersion() + if err != nil { + Error(w, http.StatusInternalServerError, "获取最新版本失败") + return + } + + v1, err := version.NewVersion(current) + if err != nil { + Error(w, http.StatusInternalServerError, "版本号解析失败") + return + } + v2, err := version.NewVersion(latest.Version) + if err != nil { + Error(w, http.StatusInternalServerError, "版本号解析失败") + return + } + if v1.GreaterThanOrEqual(v2) { + Success(w, chix.M{ + "update": false, + }) + return + } + + Success(w, chix.M{ + "update": true, + }) +} + +// UpdateInfo +// +// @Summary 版本更新信息 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/updateInfo [get] +func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) { + current := app.Conf.MustString("app.version") + latest, err := tools.GetLatestPanelVersion() + if err != nil { + Error(w, http.StatusInternalServerError, "获取最新版本失败") + return + } + + v1, err := version.NewVersion(current) + if err != nil { + Error(w, http.StatusInternalServerError, "版本号解析失败") + return + } + v2, err := version.NewVersion(latest.Version) + if err != nil { + Error(w, http.StatusInternalServerError, "版本号解析失败") + return + } + if v1.GreaterThanOrEqual(v2) { + Error(w, http.StatusInternalServerError, "当前版本已是最新版本") + return + } + + versions, err := tools.GenerateVersions(current, latest.Version) + if err != nil { + Error(w, http.StatusInternalServerError, "获取更新信息失败") + return + } + + var versionInfo []tools.PanelInfo + for _, v := range versions { + info, err := tools.GetPanelVersion(v) + if err != nil { + continue + } + + versionInfo = append(versionInfo, info) + } + + Success(w, versionInfo) +} + +// Update +// +// @Summary 更新面板 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/update [post] +func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) { + if s.taskRepo.HasRunningTask() { + Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止更新") + return + } + if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE)").Error; err != nil { + types.Status = types.StatusFailed + Error(w, http.StatusInternalServerError, fmt.Sprintf("面板数据库异常,已终止操作:%s", err.Error())) + return + } + + panel, err := tools.GetLatestPanelVersion() + if err != nil { + Error(w, http.StatusInternalServerError, "获取最新版本失败") + return + } + + types.Status = types.StatusUpgrade + if err = tools.UpdatePanel(panel); err != nil { + types.Status = types.StatusFailed + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + types.Status = types.StatusNormal + tools.RestartPanel() + Success(w, nil) +} + +// Restart +// +// @Summary 重启面板 +// @Tags 信息服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /info/restart [post] +func (s *InfoService) Restart(w http.ResponseWriter, r *http.Request) { + if s.taskRepo.HasRunningTask() { + Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止重启") + return + } + + tools.RestartPanel() + Success(w, nil) +} diff --git a/app/http/controllers/monitor_controller.go b/internal/service/monitor.go similarity index 50% rename from app/http/controllers/monitor_controller.go rename to internal/service/monitor.go index 4eb4140b..3030e5b4 100644 --- a/app/http/controllers/monitor_controller.go +++ b/internal/service/monitor.go @@ -1,103 +1,73 @@ -package controllers +package service import ( "fmt" + "net/http" - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/carbon" - "github.com/spf13/cast" + "github.com/golang-module/carbon/v2" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/pkg/h" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" ) -type MonitorController struct { - setting internal.Setting +type MonitorService struct { + settingRepo biz.SettingRepo + monitorRepo biz.MonitorRepo } -func NewMonitorController() *MonitorController { - return &MonitorController{ - setting: services.NewSettingImpl(), +func NewMonitorService() *MonitorService { + return &MonitorService{ + settingRepo: data.NewSettingRepo(), + monitorRepo: data.NewMonitorRepo(), } } -// Switch 监控开关 -func (r *MonitorController) Switch(ctx http.Context) http.Response { - value := ctx.Request().InputBool("monitor") - err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value)) +func (s *MonitorService) GetSetting(w http.ResponseWriter, r *http.Request) { + setting, err := s.monitorRepo.GetSetting() if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{ - "monitor": value, - "error": err.Error(), - }).Info("更新监控开关失败") - return h.ErrorSystem(ctx) + Error(w, http.StatusInternalServerError, err.Error()) + return } - return h.Success(ctx, nil) + Success(w, setting) } -// SaveDays 保存监控天数 -func (r *MonitorController) SaveDays(ctx http.Context) http.Response { - days := ctx.Request().Input("days") - err := r.setting.Set(models.SettingKeyMonitorDays, days) +func (s *MonitorService) UpdateSetting(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.MonitorSetting](r) if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{ - "days": days, - "error": err.Error(), - }).Info("更新监控开关失败") - return h.ErrorSystem(ctx) + Error(w, http.StatusUnprocessableEntity, err.Error()) + return } - return h.Success(ctx, nil) + if err = s.monitorRepo.UpdateSetting(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) } -// SwitchAndDays 监控开关和监控天数 -func (r *MonitorController) SwitchAndDays(ctx http.Context) http.Response { - monitor := r.setting.Get(models.SettingKeyMonitor) - monitorDays := r.setting.Get(models.SettingKeyMonitorDays) +func (s *MonitorService) Clear(w http.ResponseWriter, r *http.Request) { + if err := s.monitorRepo.Clear(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } - return h.Success(ctx, http.Json{ - "switch": cast.ToBool(monitor), - "days": cast.ToInt(monitorDays), - }) + Success(w, nil) } -// Clear 清空监控数据 -func (r *MonitorController) Clear(ctx http.Context) http.Response { - _, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{}) +func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.MonitorList](r) if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{ - "error": err.Error(), - }).Info("清空监控数据失败") - return h.ErrorSystem(ctx) + Error(w, http.StatusUnprocessableEntity, err.Error()) + return } - return h.Success(ctx, nil) -} - -// List 监控数据列表 -func (r *MonitorController) List(ctx http.Context) http.Response { - start := ctx.Request().InputInt64("start") - end := ctx.Request().InputInt64("end") - startTime := carbon.FromTimestampMilli(start) - endTime := carbon.FromTimestampMilli(end) - - var monitors []models.Monitor - err := facades.Orm().Query().Where("created_at >= ?", startTime.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors) + monitors, err := s.monitorRepo.List(carbon.CreateFromTimestampMilli(req.Start), carbon.CreateFromTimestampMilli(req.End)) if err != nil { - facades.Log().Request(ctx.Request()).Tags("面板", "资源监控").With(map[string]any{ - "start": startTime.ToDateTimeString(), - "end": endTime.ToDateTimeString(), - "error": err.Error(), - }).Info("获取监控数据失败") - return h.ErrorSystem(ctx) - } - - if len(monitors) == 0 { - return h.Error(ctx, http.StatusNotFound, "监控数据为空") + Error(w, http.StatusInternalServerError, err.Error()) + return } type load struct { @@ -182,5 +152,5 @@ func (r *MonitorController) List(ctx http.Context) http.Response { bytesRecv2 = 0 } - return h.Success(ctx, data) + Success(w, data) } diff --git a/internal/service/plugin.go b/internal/service/plugin.go new file mode 100644 index 00000000..1309283c --- /dev/null +++ b/internal/service/plugin.go @@ -0,0 +1,160 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type PluginService struct { + pluginRepo biz.PluginRepo +} + +func NewPluginService() *PluginService { + return &PluginService{ + pluginRepo: data.NewPluginRepo(), + } +} + +func (s *PluginService) List(w http.ResponseWriter, r *http.Request) { + plugins := s.pluginRepo.All() + installedPlugins, err := s.pluginRepo.Installed() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + installedPluginsMap := make(map[string]*biz.Plugin) + + for _, p := range installedPlugins { + installedPluginsMap[p.Slug] = p + } + + type plugin struct { + Name string `json:"name"` + Description string `json:"description"` + Slug string `json:"slug"` + Version string `json:"version"` + Requires []string `json:"requires"` + Excludes []string `json:"excludes"` + Installed bool `json:"installed"` + InstalledVersion string `json:"installed_version"` + Show bool `json:"show"` + } + + var pluginArr []plugin + for _, item := range plugins { + installed, installedVersion, show := false, "", false + if _, ok := installedPluginsMap[item.Slug]; ok { + installed = true + installedVersion = installedPluginsMap[item.Slug].Version + show = installedPluginsMap[item.Slug].Show + } + pluginArr = append(pluginArr, plugin{ + Name: item.Name, + Description: item.Description, + Slug: item.Slug, + Version: item.Version, + Requires: item.Requires, + Excludes: item.Excludes, + Installed: installed, + InstalledVersion: installedVersion, + Show: show, + }) + } + + paged, total := Paginate(r, pluginArr) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +func (s *PluginService) Install(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.pluginRepo.Install(req.Slug); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *PluginService) Uninstall(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.pluginRepo.Uninstall(req.Slug); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *PluginService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.pluginRepo.Update(req.Slug); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *PluginService) UpdateShow(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginUpdateShow](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.pluginRepo.UpdateShow(req.Slug, req.Show); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *PluginService) IsInstalled(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PluginSlug](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + plugin, err := s.pluginRepo.Get(req.Slug) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + installed, err := s.pluginRepo.IsInstalled(req.Slug) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "name": plugin.Name, + "installed": installed, + }) +} diff --git a/internal/service/safe.go b/internal/service/safe.go new file mode 100644 index 00000000..8e3f5312 --- /dev/null +++ b/internal/service/safe.go @@ -0,0 +1,73 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type SafeService struct { + safeRepo biz.SafeRepo +} + +func NewSafeService() *SafeService { + return &SafeService{ + safeRepo: data.NewSafeRepo(), + } +} + +func (s *SafeService) GetSSH(w http.ResponseWriter, r *http.Request) { + port, status, err := s.safeRepo.GetSSH() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + Success(w, chix.M{ + "port": port, + "status": status, + }) +} + +func (s *SafeService) UpdateSSH(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SafeUpdateSSH](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.safeRepo.UpdateSSH(req.Port, req.Status); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +func (s *SafeService) GetPingStatus(w http.ResponseWriter, r *http.Request) { + status, err := s.safeRepo.GetPingStatus() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, status) +} + +func (s *SafeService) UpdatePingStatus(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SafeUpdatePingStatus](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.safeRepo.UpdatePingStatus(req.Status); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/setting.go b/internal/service/setting.go new file mode 100644 index 00000000..f30d2ac8 --- /dev/null +++ b/internal/service/setting.go @@ -0,0 +1,44 @@ +package service + +import ( + "net/http" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type SettingService struct { + settingRepo biz.SettingRepo +} + +func NewSettingService() *SettingService { + return &SettingService{ + settingRepo: data.NewSettingRepo(), + } +} + +func (s *SettingService) Get(w http.ResponseWriter, r *http.Request) { + setting, err := s.settingRepo.GetPanelSetting() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, setting) +} + +func (s *SettingService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.PanelSetting](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.settingRepo.UpdatePanelSetting(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/ssh.go b/internal/service/ssh.go new file mode 100644 index 00000000..b3b700a9 --- /dev/null +++ b/internal/service/ssh.go @@ -0,0 +1,127 @@ +package service + +import ( + "bytes" + "context" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/ssh" +) + +type SSHService struct { + sshRepo biz.SSHRepo +} + +func NewSSHService() *SSHService { + return &SSHService{ + sshRepo: data.NewSSHRepo(), + } +} + +func (s *SSHService) GetInfo(w http.ResponseWriter, r *http.Request) { + info, err := s.sshRepo.GetInfo() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, info) +} + +func (s *SSHService) UpdateInfo(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SSHUpdateInfo](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.sshRepo.UpdateInfo(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } +} + +func (s *SSHService) Session(w http.ResponseWriter, r *http.Request) { + info, err := s.sshRepo.GetInfo() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + upGrader := websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + ws, err := upGrader.Upgrade(w, r, nil) + if err != nil { + ErrorSystem(w) + return + } + defer ws.Close() + + config := ssh.ClientConfigPassword( + cast.ToString(info["host"])+":"+cast.ToString(info["port"]), + cast.ToString(info["user"]), + cast.ToString(info["password"]), + ) + client, err := ssh.NewSSHClient(config) + + if err != nil { + _ = ws.WriteControl(websocket.CloseMessage, + []byte(err.Error()), time.Now().Add(time.Second)) + ErrorSystem(w) + return + } + defer client.Close() + + turn, err := ssh.NewTurn(ws, client) + if err != nil { + _ = ws.WriteControl(websocket.CloseMessage, + []byte(err.Error()), time.Now().Add(time.Second)) + ErrorSystem(w) + return + } + defer turn.Close() + + var bufPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, + } + var logBuff = bufPool.Get().(*bytes.Buffer) + logBuff.Reset() + defer bufPool.Put(logBuff) + + sshCtx, cancel := context.WithCancel(context.Background()) + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + defer wg.Done() + if err = turn.LoopRead(logBuff, sshCtx); err != nil { + ErrorSystem(w) + return + } + }() + go func() { + defer wg.Done() + if err = turn.SessionWait(); err != nil { + ErrorSystem(w) + return + } + cancel() + }() + wg.Wait() + +} diff --git a/internal/service/systemctl.go b/internal/service/systemctl.go new file mode 100644 index 00000000..0b299efc --- /dev/null +++ b/internal/service/systemctl.go @@ -0,0 +1,138 @@ +package service + +import ( + "fmt" + "net/http" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type SystemctlService struct { +} + +func NewSystemctlService() *SystemctlService { + return &SystemctlService{} +} + +func (s *SystemctlService) Status(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + status, err := systemctl.Status(req.Service) + if err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", req.Service)) + return + } + + Success(w, status) +} + +func (s *SystemctlService) IsEnabled(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + enabled, err := systemctl.IsEnabled(req.Service) + if err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务启用状态失败", req.Service)) + return + } + + Success(w, enabled) +} + +func (s *SystemctlService) Enable(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Enable(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("启用 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Disable(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Disable(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("禁用 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Restart(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Restart(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Reload(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Reload(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("重载 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Start(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Start(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("启动 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} + +func (s *SystemctlService) Stop(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Stop(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("停止 %s 服务失败", req.Service)) + return + } + + Success(w, nil) +} diff --git a/internal/service/task.go b/internal/service/task.go new file mode 100644 index 00000000..49fc1269 --- /dev/null +++ b/internal/service/task.go @@ -0,0 +1,116 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/shell" +) + +type TaskService struct { + taskRepo biz.TaskRepo +} + +func NewTaskService() *TaskService { + return &TaskService{ + taskRepo: data.NewTaskRepo(), + } +} + +// Status +// +// @Summary 是否有任务正在运行 +// @Tags 任务服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /tasks/status [get] +func (s *TaskService) Status(w http.ResponseWriter, r *http.Request) { + Success(w, chix.M{ + "task": s.taskRepo.HasRunningTask(), + }) +} + +// List +// +// @Summary 任务列表 +// @Tags 任务服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /tasks [get] +func (s *TaskService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + tasks, total, err := s.taskRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": tasks, + }) +} + +// Get +// +// @Summary 任务详情 +// @Tags 任务服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /task/log [get] +func (s *TaskService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + task, err := s.taskRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + log, err := shell.Execf(`tail -n 500 '%s'`, task.Log) + if err == nil { + task.Log = log + } + + Success(w, task) +} + +// Delete +// +// @Summary 删除任务 +// @Tags 任务服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /task/delete [post] +func (s *TaskService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + err = s.taskRepo.Delete(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/service/user.go b/internal/service/user.go new file mode 100644 index 00000000..ab093318 --- /dev/null +++ b/internal/service/user.go @@ -0,0 +1,117 @@ +package service + +import ( + "net/http" + + "github.com/go-rat/chix" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) + +type UserService struct { + repo biz.UserRepo +} + +func NewUserService() *UserService { + return &UserService{ + repo: data.NewUserRepo(), + } +} + +// Login +// +// @Summary 登录 +// @Tags 用户服务 +// @Accept json +// @Produce json +// @Param data body request.UserLogin true "request" +// @Success 200 {object} SuccessResponse +// @Router /user/login [post] +func (s *UserService) Login(w http.ResponseWriter, r *http.Request) { + sess, err := app.Session.GetSession(r) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + req, err := Bind[request.UserLogin](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + user, err := s.repo.CheckPassword(req.Username, req.Password) + if err != nil { + Error(w, http.StatusForbidden, err.Error()) + return + } + + sess.Put("user_id", user.ID) + Success(w, nil) +} + +// Logout +// +// @Summary 登出 +// @Tags 用户服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /user/logout [post] +func (s *UserService) Logout(w http.ResponseWriter, r *http.Request) { + sess, err := app.Session.GetSession(r) + if err == nil { + sess.Forget("user_id") + } + Success(w, nil) +} + +// IsLogin +// +// @Summary 是否登录 +// @Tags 用户服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /user/isLogin [get] +func (s *UserService) IsLogin(w http.ResponseWriter, r *http.Request) { + sess, err := app.Session.GetSession(r) + if err != nil { + Success(w, false) + return + } + Success(w, sess.Has("user_id")) +} + +// Info +// +// @Summary 用户信息 +// @Tags 用户服务 +// @Accept json +// @Produce json +// @Success 200 {object} SuccessResponse +// @Router /user/info/{id} [get] +func (s *UserService) Info(w http.ResponseWriter, r *http.Request) { + userID := cast.ToUint(r.Context().Value("user_id")) + if userID == 0 { + ErrorSystem(w) + return + } + + user, err := s.repo.Get(userID) + if err != nil { + ErrorSystem(w) + return + } + + Success(w, chix.M{ + "id": user.ID, + "role": []string{"admin"}, + "username": user.Username, + "email": user.Email, + }) +} diff --git a/internal/service/website.go b/internal/service/website.go new file mode 100644 index 00000000..1b1e2aed --- /dev/null +++ b/internal/service/website.go @@ -0,0 +1,301 @@ +package service + +import ( + "net/http" + "path/filepath" + + "github.com/go-rat/chix" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/io" +) + +type WebsiteService struct { + websiteRepo biz.WebsiteRepo + settingRepo biz.SettingRepo +} + +func NewWebsiteService() *WebsiteService { + return &WebsiteService{ + websiteRepo: data.NewWebsiteRepo(), + settingRepo: data.NewSettingRepo(), + } +} + +// GetDefaultConfig +// +// @Summary 获取默认配置 +// @Tags 网站服务 +// @Produce json +// @Success 200 {object} SuccessResponse{data=map[string]string} +// @Router /panel/website/defaultConfig [get] +func (s *WebsiteService) GetDefaultConfig(w http.ResponseWriter, r *http.Request) { + index, err := io.Read(filepath.Join(app.Root, "server/openresty/html/index.html")) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + stop, err := io.Read(filepath.Join(app.Root, "server/openresty/html/stop.html")) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "index": index, + "stop": stop, + }) +} + +// UpdateDefaultConfig +// +// @Summary 更新默认配置 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param data body map[string]string true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/website/defaultConfig [post] +func (s *WebsiteService) UpdateDefaultConfig(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteDefaultConfig](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.UpdateDefaultConfig(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// List +// +// @Summary 网站列表 +// @Tags 网站服务 +// @Produce json +// @Param data query commonrequests.Paginate true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites [get] +func (s *WebsiteService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.Paginate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + websites, total, err := s.websiteRepo.List(req.Page, req.Limit) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, chix.M{ + "total": total, + "items": websites, + }) +} + +// Create +// +// @Summary 创建网站 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param data body requests.Add true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites [post] +func (s *WebsiteService) Create(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteCreate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if len(req.Path) == 0 { + req.Path, _ = s.settingRepo.Get(biz.SettingKeyWebsitePath) + req.Path = filepath.Join(req.Path, req.Name) + } + + if _, err = s.websiteRepo.Create(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Get +// +// @Summary 获取网站 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse{data=types.WebsiteAdd} +// @Router /panel/websites/{id}/config [get] +func (s *WebsiteService) Get(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + config, err := s.websiteRepo.Get(req.ID) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, config) +} + +// Update +// +// @Summary 更新网站 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Param data body requests.SaveConfig true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/config [post] +func (s *WebsiteService) Update(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteUpdate](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.Update(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// Delete +// +// @Summary 删除网站 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param data body requests.Delete true "request" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/delete [post] +func (s *WebsiteService) Delete(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteDelete](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.Delete(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// ClearLog +// +// @Summary 清空网站日志 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/log [delete] +func (s *WebsiteService) ClearLog(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.ClearLog(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// UpdateRemark +// +// @Summary 更新网站备注 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/updateRemark [post] +func (s *WebsiteService) UpdateRemark(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteUpdateRemark](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.UpdateRemark(req.ID, req.Remark); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// ResetConfig +// +// @Summary 重置网站配置 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/resetConfig [post] +func (s *WebsiteService) ResetConfig(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.ID](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.ResetConfig(req.ID); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} + +// UpdateStatus +// +// @Summary 更新网站状态 +// @Tags 网站服务 +// @Accept json +// @Produce json +// @Param id path int true "网站 ID" +// @Success 200 {object} SuccessResponse +// @Router /panel/websites/{id}/status [post] +func (s *WebsiteService) UpdateStatus(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.WebsiteUpdateStatus](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.websiteRepo.UpdateStatus(req.ID, req.Status); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) +} diff --git a/internal/services/backup.go b/internal/services/backup.go deleted file mode 100644 index 6144b8e2..00000000 --- a/internal/services/backup.go +++ /dev/null @@ -1,331 +0,0 @@ -// Package services 备份服务 -package services - -import ( - "errors" - "os" - "path/filepath" - "strings" - - "github.com/goravel/framework/support/carbon" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type BackupImpl struct { - setting internal.Setting -} - -func NewBackupImpl() *BackupImpl { - return &BackupImpl{ - setting: NewSettingImpl(), - } -} - -// WebsiteList 网站备份列表 -func (s *BackupImpl) WebsiteList() ([]types.BackupFile, error) { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return []types.BackupFile{}, nil - } - - backupPath += "/website" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return []types.BackupFile{}, err - } - } - - files, err := os.ReadDir(backupPath) - if err != nil { - return []types.BackupFile{}, err - } - var backupList []types.BackupFile - for _, file := range files { - info, err := file.Info() - if err != nil { - continue - } - backupList = append(backupList, types.BackupFile{ - Name: file.Name(), - Size: str.FormatBytes(float64(info.Size())), - }) - } - - return backupList, nil -} - -// WebSiteBackup 网站备份 -func (s *BackupImpl) WebSiteBackup(website models.Website) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return errors.New("未正确配置备份路径") - } - - backupPath += "/website" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return err - } - } - - backupFile := backupPath + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip" - if _, err := shell.Execf(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`); err != nil { - return err - } - - return nil -} - -// WebsiteRestore 网站恢复 -func (s *BackupImpl) WebsiteRestore(website models.Website, backupFile string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return errors.New("未正确配置备份路径") - } - - backupPath += "/website" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return err - } - } - - backupFile = backupPath + "/" + backupFile - if !io.Exists(backupFile) { - return errors.New("备份文件不存在") - } - - if err := io.Remove(website.Path); err != nil { - return err - } - if err := io.UnArchive(backupFile, website.Path); err != nil { - return err - } - if err := io.Chmod(website.Path, 0755); err != nil { - return err - } - if err := io.Chown(website.Path, "www", "www"); err != nil { - return err - } - - return nil -} - -// MysqlList MySQL备份列表 -func (s *BackupImpl) MysqlList() ([]types.BackupFile, error) { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return []types.BackupFile{}, nil - } - - backupPath += "/mysql" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return []types.BackupFile{}, err - } - } - - files, err := os.ReadDir(backupPath) - if err != nil { - return []types.BackupFile{}, err - } - var backupList []types.BackupFile - for _, file := range files { - info, err := file.Info() - if err != nil { - continue - } - backupList = append(backupList, types.BackupFile{ - Name: file.Name(), - Size: str.FormatBytes(float64(info.Size())), - }) - } - - return backupList, nil -} - -// MysqlBackup MySQL备份 -func (s *BackupImpl) MysqlBackup(database string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/mysql" - rootPassword := s.setting.Get(models.SettingKeyMysqlRootPassword) - backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return err - } - } - err := os.Setenv("MYSQL_PWD", rootPassword) - if err != nil { - return err - } - - if _, err := shell.Execf("mysqldump -uroot " + database + " > " + backupPath + "/" + backupFile); err != nil { - return err - } - if _, err := shell.Execf("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile); err != nil { - return err - } - if err := io.Remove(backupPath + "/" + backupFile); err != nil { - return err - } - - return os.Unsetenv("MYSQL_PWD") -} - -// MysqlRestore MySQL恢复 -func (s *BackupImpl) MysqlRestore(database string, backupFile string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/mysql" - rootPassword := s.setting.Get(models.SettingKeyMysqlRootPassword) - backupFullPath := filepath.Join(backupPath, backupFile) - if !io.Exists(backupFullPath) { - return errors.New("备份文件不存在") - } - - if err := os.Setenv("MYSQL_PWD", rootPassword); err != nil { - return err - } - - tempDir, err := io.TempDir(backupFile) - if err != nil { - return err - } - - if !strings.HasSuffix(backupFile, ".sql") { - backupFile = "" // 置空,防止干扰后续判断 - if err = io.UnArchive(backupFullPath, tempDir); err != nil { - return err - } - if files, err := os.ReadDir(tempDir); err == nil { - for _, file := range files { - if strings.HasSuffix(file.Name(), ".sql") { - backupFile = filepath.Base(file.Name()) - break - } - } - } - } else { - if err = io.Cp(backupFullPath, filepath.Join(tempDir, backupFile)); err != nil { - return err - } - } - - if len(backupFile) == 0 { - return errors.New("无法找到备份文件") - } - - if _, err = shell.Execf("mysql -uroot " + database + " < " + filepath.Join(tempDir, backupFile)); err != nil { - return err - } - - if err = io.Remove(tempDir); err != nil { - return err - } - - return os.Unsetenv("MYSQL_PWD") -} - -// PostgresqlList PostgreSQL备份列表 -func (s *BackupImpl) PostgresqlList() ([]types.BackupFile, error) { - backupPath := s.setting.Get(models.SettingKeyBackupPath) - if len(backupPath) == 0 { - return []types.BackupFile{}, nil - } - - backupPath += "/postgresql" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return []types.BackupFile{}, err - } - } - - files, err := os.ReadDir(backupPath) - if err != nil { - return []types.BackupFile{}, err - } - var backupList []types.BackupFile - for _, file := range files { - info, err := file.Info() - if err != nil { - continue - } - backupList = append(backupList, types.BackupFile{ - Name: file.Name(), - Size: str.FormatBytes(float64(info.Size())), - }) - } - - return backupList, nil -} - -// PostgresqlBackup PostgreSQL备份 -func (s *BackupImpl) PostgresqlBackup(database string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/postgresql" - backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql" - if !io.Exists(backupPath) { - if err := io.Mkdir(backupPath, 0644); err != nil { - return err - } - } - - if _, err := shell.Execf(`su - postgres -c "pg_dump ` + database + `" > ` + backupPath + "/" + backupFile); err != nil { - return err - } - if _, err := shell.Execf("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile); err != nil { - return err - } - - return io.Remove(backupPath + "/" + backupFile) -} - -// PostgresqlRestore PostgreSQL恢复 -func (s *BackupImpl) PostgresqlRestore(database string, backupFile string) error { - backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/postgresql" - backupFullPath := filepath.Join(backupPath, backupFile) - if !io.Exists(backupFullPath) { - return errors.New("备份文件不存在") - } - - tempDir, err := io.TempDir(backupFile) - if err != nil { - return err - } - - if !strings.HasSuffix(backupFile, ".sql") { - backupFile = "" // 置空,防止干扰后续判断 - if err = io.UnArchive(backupFullPath, tempDir); err != nil { - return err - } - if files, err := os.ReadDir(tempDir); err == nil { - for _, file := range files { - if strings.HasSuffix(file.Name(), ".sql") { - backupFile = filepath.Base(file.Name()) - break - } - } - } - } else { - if err = io.Cp(backupFullPath, filepath.Join(tempDir, backupFile)); err != nil { - return err - } - } - - if len(backupFile) == 0 { - return errors.New("无法找到备份文件") - } - - if _, err = shell.Execf(`su - postgres -c "psql ` + database + `" < ` + filepath.Join(tempDir, backupFile)); err != nil { - return err - } - - if err = io.Remove(tempDir); err != nil { - return err - } - - return nil -} diff --git a/internal/services/cert.go b/internal/services/cert.go deleted file mode 100644 index 927bb123..00000000 --- a/internal/services/cert.go +++ /dev/null @@ -1,508 +0,0 @@ -// Package services 证书服务 -package services - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - "github.com/go-resty/resty/v2" - "github.com/goravel/framework/facades" - - requests "github.com/TheTNB/panel/v2/app/http/requests/cert" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/acme" - "github.com/TheTNB/panel/v2/pkg/cert" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type CertImpl struct { - client *acme.Client -} - -func NewCertImpl() *CertImpl { - return &CertImpl{} -} - -// UserStore 添加用户 -func (s *CertImpl) UserStore(request requests.UserStore) error { - var user models.CertUser - user.CA = request.CA - user.Email = request.Email - user.Kid = request.Kid - user.HmacEncoded = request.HmacEncoded - user.KeyType = request.KeyType - - var err error - var client *acme.Client - switch user.CA { - case "letsencrypt": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CALetsEncrypt, nil, acme.KeyType(user.KeyType)) - case "buypass": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CABuypass, nil, acme.KeyType(user.KeyType)) - case "zerossl": - eab, eabErr := s.getZeroSSLEAB(user.Email) - if eabErr != nil { - return eabErr - } - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAZeroSSL, eab, acme.KeyType(user.KeyType)) - case "sslcom": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CASSLcom, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType)) - case "google": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAGoogle, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType)) - default: - return errors.New("CA 提供商不支持") - } - - if err != nil { - return errors.New("向 CA 注册账号失败,请检查参数是否正确") - } - - privateKey, err := cert.EncodeKey(client.Account.PrivateKey) - if err != nil { - return errors.New("获取私钥失败") - } - user.PrivateKey = string(privateKey) - - return facades.Orm().Query().Create(&user) -} - -// UserUpdate 更新用户 -func (s *CertImpl) UserUpdate(request requests.UserUpdate) error { - var user models.CertUser - err := facades.Orm().Query().Where("id = ?", request.ID).First(&user) - if err != nil { - return err - } - - user.CA = request.CA - user.Email = request.Email - user.Kid = request.Kid - user.HmacEncoded = request.HmacEncoded - user.KeyType = request.KeyType - - var client *acme.Client - switch user.CA { - case "letsencrypt": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CALetsEncrypt, nil, acme.KeyType(user.KeyType)) - case "buypass": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CABuypass, nil, acme.KeyType(user.KeyType)) - case "zerossl": - eab, eabErr := s.getZeroSSLEAB(user.Email) - if eabErr != nil { - return eabErr - } - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAZeroSSL, eab, acme.KeyType(user.KeyType)) - case "sslcom": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CASSLcom, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType)) - case "google": - client, err = acme.NewRegisterAccount(context.Background(), user.Email, acme.CAGoogle, &acme.EAB{KeyID: user.Kid, MACKey: user.HmacEncoded}, acme.KeyType(user.KeyType)) - default: - return errors.New("CA 提供商不支持") - } - - if err != nil { - return errors.New("向 CA 注册账号失败,请检查参数是否正确") - } - - privateKey, err := cert.EncodeKey(client.Account.PrivateKey) - if err != nil { - return errors.New("获取私钥失败") - } - user.PrivateKey = string(privateKey) - - return facades.Orm().Query().Save(&user) -} - -// getZeroSSLEAB 获取 ZeroSSL EAB -func (s *CertImpl) getZeroSSLEAB(email string) (*acme.EAB, error) { - type data struct { - Success bool `json:"success"` - EabKid string `json:"eab_kid"` - EabHmacKey string `json:"eab_hmac_key"` - } - client := resty.New() - client.SetTimeout(5 * time.Second) - client.SetRetryCount(2) - - resp, err := client.R().SetFormData(map[string]string{ - "email": email, - }).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email") - if err != nil || !resp.IsSuccess() { - return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败") - } - eab := resp.Result().(*data) - if !eab.Success { - return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败") - } - - return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil -} - -// UserShow 根据 ID 获取用户 -func (s *CertImpl) UserShow(ID uint) (models.CertUser, error) { - var user models.CertUser - err := facades.Orm().Query().With("Certs").Where("id = ?", ID).First(&user) - - return user, err -} - -// UserDestroy 删除用户 -func (s *CertImpl) UserDestroy(ID uint) error { - var cert models.Cert - err := facades.Orm().Query().Where("user_id = ?", ID).First(&cert) - if err != nil { - return err - } - - if cert.ID != 0 { - return errors.New("该用户下存在证书,无法删除") - } - - _, err = facades.Orm().Query().Delete(&models.CertUser{}, ID) - return err -} - -// DNSStore 添加 DNS -func (s *CertImpl) DNSStore(request requests.DNSStore) error { - var dns models.CertDNS - dns.Type = request.Type - dns.Name = request.Name - dns.Data = request.Data - - return facades.Orm().Query().Create(&dns) -} - -// DNSUpdate 更新 DNS -func (s *CertImpl) DNSUpdate(request requests.DNSUpdate) error { - var dns models.CertDNS - err := facades.Orm().Query().Where("id = ?", request.ID).First(&dns) - if err != nil { - return err - } - - dns.Type = request.Type - dns.Name = request.Name - dns.Data = request.Data - - return facades.Orm().Query().Save(&dns) -} - -// DNSShow 根据 ID 获取 DNS -func (s *CertImpl) DNSShow(ID uint) (models.CertDNS, error) { - var dns models.CertDNS - err := facades.Orm().Query().With("Certs").Where("id = ?", ID).First(&dns) - - return dns, err -} - -// DNSDestroy 删除 DNS -func (s *CertImpl) DNSDestroy(ID uint) error { - var cert models.Cert - err := facades.Orm().Query().Where("dns_id = ?", ID).First(&cert) - if err != nil { - return err - } - - if cert.ID != 0 { - return errors.New("该 DNS 接口下存在证书,无法删除") - } - - _, err = facades.Orm().Query().Delete(&models.CertDNS{}, ID) - return err -} - -// CertStore 添加证书 -func (s *CertImpl) CertStore(request requests.CertStore) error { - var cert models.Cert - cert.Type = request.Type - cert.Domains = request.Domains - cert.AutoRenew = request.AutoRenew - cert.UserID = request.UserID - cert.DNSID = request.DNSID - cert.WebsiteID = request.WebsiteID - - return facades.Orm().Query().Create(&cert) -} - -// CertUpdate 更新证书 -func (s *CertImpl) CertUpdate(request requests.CertUpdate) error { - var cert models.Cert - err := facades.Orm().Query().Where("id = ?", request.ID).First(&cert) - if err != nil { - return err - } - - cert.Type = request.Type - cert.Domains = request.Domains - cert.AutoRenew = request.AutoRenew - cert.UserID = request.UserID - cert.DNSID = request.DNSID - cert.WebsiteID = request.WebsiteID - - return facades.Orm().Query().Save(&cert) -} - -// CertShow 根据 ID 获取证书 -func (s *CertImpl) CertShow(ID uint) (models.Cert, error) { - var cert models.Cert - err := facades.Orm().Query().With("User").With("DNS").With("Website").Where("id = ?", ID).First(&cert) - - return cert, err -} - -// CertDestroy 删除证书 -func (s *CertImpl) CertDestroy(ID uint) error { - var cert models.Cert - err := facades.Orm().Query().Where("id = ?", ID).First(&cert) - if err != nil { - return err - } - - _, err = facades.Orm().Query().Delete(&models.Cert{}, ID) - return err -} - -// ObtainAuto 自动签发证书 -func (s *CertImpl) ObtainAuto(ID uint) (acme.Certificate, error) { - var cert models.Cert - err := facades.Orm().Query().With("Website").With("User").With("DNS").Where("id = ?", ID).First(&cert) - if err != nil { - return acme.Certificate{}, err - } - - client, err := s.getClient(cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.DNS != nil { - client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) - } else { - if cert.Website == nil { - return acme.Certificate{}, errors.New("该证书没有关联网站,无法自动签发") - } else { - for _, domain := range cert.Domains { - if strings.Contains(domain, "*") { - return acme.Certificate{}, errors.New("通配符域名无法使用 HTTP 验证") - } - } - conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name) - client.UseHTTP(conf, cert.Website.Path) - } - } - - ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type)) - if err != nil { - return acme.Certificate{}, err - } - - cert.CertURL = ssl.URL - cert.Cert = string(ssl.ChainPEM) - cert.Key = string(ssl.PrivateKey) - err = facades.Orm().Query().Save(&cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.Website != nil { - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".pem", cert.Cert, 0644); err != nil { - return acme.Certificate{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".key", cert.Key, 0644); err != nil { - return acme.Certificate{}, err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return acme.Certificate{}, err - } - } - - return ssl, nil -} - -// ObtainManual 手动签发证书 -func (s *CertImpl) ObtainManual(ID uint) (acme.Certificate, error) { - var cert models.Cert - err := facades.Orm().Query().With("User").Where("id = ?", ID).First(&cert) - if err != nil { - return acme.Certificate{}, err - } - - if s.client == nil { - return acme.Certificate{}, errors.New("请重新获取 DNS 解析记录") - } - - ssl, err := s.client.ObtainSSLManual() - if err != nil { - return acme.Certificate{}, err - } - - cert.CertURL = ssl.URL - cert.Cert = string(ssl.ChainPEM) - cert.Key = string(ssl.PrivateKey) - err = facades.Orm().Query().Save(&cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.Website != nil { - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".pem", cert.Cert, 0644); err != nil { - return acme.Certificate{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".key", cert.Key, 0644); err != nil { - return acme.Certificate{}, err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return acme.Certificate{}, err - } - } - - return ssl, nil -} - -// ManualDNS 获取手动 DNS 解析信息 -func (s *CertImpl) ManualDNS(ID uint) ([]acme.DNSRecord, error) { - var cert models.Cert - err := facades.Orm().Query().With("User").Where("id = ?", ID).First(&cert) - if err != nil { - return nil, err - } - - client, err := s.getClient(cert) - if err != nil { - return nil, err - } - - client.UseManualDns(len(cert.Domains)) - records, err := client.GetDNSRecords(context.Background(), cert.Domains, acme.KeyType(cert.Type)) - - // 15 分钟后清理客户端 - s.client = client - time.AfterFunc(15*time.Minute, func() { - s.client = nil - }) - - return records, err -} - -// Renew 续签证书 -func (s *CertImpl) Renew(ID uint) (acme.Certificate, error) { - var cert models.Cert - err := facades.Orm().Query().With("Website").With("User").With("DNS").Where("id = ?", ID).First(&cert) - if err != nil { - return acme.Certificate{}, err - } - - client, err := s.getClient(cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.CertURL == "" { - return acme.Certificate{}, errors.New("该证书没有签发成功,无法续签") - } - - if cert.DNS != nil { - client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data) - } else { - if cert.Website == nil { - return acme.Certificate{}, errors.New("该证书没有关联网站,无法续签,可以尝试手动签发") - } else { - for _, domain := range cert.Domains { - if strings.Contains(domain, "*") { - return acme.Certificate{}, errors.New("通配符域名无法使用 HTTP 验证") - } - } - conf := fmt.Sprintf("/www/server/vhost/acme/%s.conf", cert.Website.Name) - client.UseHTTP(conf, cert.Website.Path) - } - } - - ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type)) - if err != nil { - return acme.Certificate{}, err - } - - cert.CertURL = ssl.URL - cert.Cert = string(ssl.ChainPEM) - cert.Key = string(ssl.PrivateKey) - err = facades.Orm().Query().Save(&cert) - if err != nil { - return acme.Certificate{}, err - } - - if cert.Website != nil { - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".pem", cert.Cert, 0644); err != nil { - return acme.Certificate{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+cert.Website.Name+".key", cert.Key, 0644); err != nil { - return acme.Certificate{}, err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return acme.Certificate{}, err - } - } - - return ssl, nil -} - -// Deploy 部署证书 -func (s *CertImpl) Deploy(ID, WebsiteID uint) error { - var cert models.Cert - err := facades.Orm().Query().Where("id = ?", ID).First(&cert) - if err != nil { - return err - } - - if cert.Cert == "" || cert.Key == "" { - return errors.New("该证书没有签发成功,无法部署") - } - - website := models.Website{} - err = facades.Orm().Query().Where("id = ?", WebsiteID).First(&website) - if err != nil { - return err - } - - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".pem", cert.Cert, 0644); err != nil { - return err - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".key", cert.Key, 0644); err != nil { - return err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return err - } - - return nil -} - -func (s *CertImpl) getClient(cert models.Cert) (*acme.Client, error) { - var ca string - var eab *acme.EAB - switch cert.User.CA { - case "letsencrypt": - ca = acme.CALetsEncrypt - case "buypass": - ca = acme.CABuypass - case "zerossl": - ca = acme.CAZeroSSL - eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded} - case "sslcom": - ca = acme.CASSLcom - eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded} - case "google": - ca = acme.CAGoogle - eab = &acme.EAB{KeyID: cert.User.Kid, MACKey: cert.User.HmacEncoded} - } - - return acme.NewPrivateKeyAccount(cert.User.Email, cert.User.PrivateKey, ca, eab) -} diff --git a/internal/services/container.go b/internal/services/container.go deleted file mode 100644 index 3bcd7bf8..00000000 --- a/internal/services/container.go +++ /dev/null @@ -1,374 +0,0 @@ -package services - -import ( - "context" - "encoding/base64" - "io" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" - "github.com/goravel/framework/support/json" - - requests "github.com/TheTNB/panel/v2/app/http/requests/container" - paneltypes "github.com/TheTNB/panel/v2/pkg/types" -) - -type Container struct { - client *client.Client -} - -func NewContainer(sock ...string) Container { - if len(sock) == 0 { - sock = append(sock, "/run/podman/podman.sock") - } - cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) - return Container{ - client: cli, - } -} - -// ContainerListAll 列出所有容器 -func (r *Container) ContainerListAll() ([]types.Container, error) { - containers, err := r.client.ContainerList(context.Background(), container.ListOptions{ - All: true, - }) - if err != nil { - return nil, err - } - - return containers, nil -} - -// ContainerListByNames 根据名称列出容器 -func (r *Container) ContainerListByNames(names []string) ([]types.Container, error) { - var options container.ListOptions - options.All = true - if len(names) > 0 { - var array []filters.KeyValuePair - for _, n := range names { - array = append(array, filters.Arg("name", n)) - } - options.Filters = filters.NewArgs(array...) - } - containers, err := r.client.ContainerList(context.Background(), options) - if err != nil { - return nil, err - } - - return containers, nil -} - -// ContainerCreate 创建容器 -func (r *Container) ContainerCreate(name string, config container.Config, host container.HostConfig, network network.NetworkingConfig) (string, error) { - resp, err := r.client.ContainerCreate(context.Background(), &config, &host, &network, nil, name) - return resp.ID, err -} - -// ContainerRemove 移除容器 -func (r *Container) ContainerRemove(id string) error { - return r.client.ContainerRemove(context.Background(), id, container.RemoveOptions{ - Force: true, - }) -} - -// ContainerStart 启动容器 -func (r *Container) ContainerStart(id string) error { - return r.client.ContainerStart(context.Background(), id, container.StartOptions{}) -} - -// ContainerStop 停止容器 -func (r *Container) ContainerStop(id string) error { - return r.client.ContainerStop(context.Background(), id, container.StopOptions{}) -} - -// ContainerRestart 重启容器 -func (r *Container) ContainerRestart(id string) error { - return r.client.ContainerRestart(context.Background(), id, container.StopOptions{}) -} - -// ContainerPause 暂停容器 -func (r *Container) ContainerPause(id string) error { - return r.client.ContainerPause(context.Background(), id) -} - -// ContainerUnpause 恢复容器 -func (r *Container) ContainerUnpause(id string) error { - return r.client.ContainerUnpause(context.Background(), id) -} - -// ContainerInspect 查看容器 -func (r *Container) ContainerInspect(id string) (types.ContainerJSON, error) { - return r.client.ContainerInspect(context.Background(), id) -} - -// ContainerKill 杀死容器 -func (r *Container) ContainerKill(id string) error { - return r.client.ContainerKill(context.Background(), id, "KILL") -} - -// ContainerRename 重命名容器 -func (r *Container) ContainerRename(id string, newName string) error { - return r.client.ContainerRename(context.Background(), id, newName) -} - -// ContainerStats 查看容器状态 -func (r *Container) ContainerStats(id string) (container.StatsResponseReader, error) { - return r.client.ContainerStats(context.Background(), id, false) -} - -// ContainerExist 判断容器是否存在 -func (r *Container) ContainerExist(name string) (bool, error) { - var options container.ListOptions - options.Filters = filters.NewArgs(filters.Arg("name", name)) - containers, err := r.client.ContainerList(context.Background(), options) - if err != nil { - return false, err - } - - return len(containers) > 0, nil -} - -// ContainerUpdate 更新容器 -func (r *Container) ContainerUpdate(id string, config container.UpdateConfig) error { - _, err := r.client.ContainerUpdate(context.Background(), id, config) - return err -} - -// ContainerLogs 查看容器日志 -func (r *Container) ContainerLogs(id string) (string, error) { - options := container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - } - reader, err := r.client.ContainerLogs(context.Background(), id, options) - if err != nil { - return "", err - } - defer reader.Close() - - data, err := io.ReadAll(reader) - if err != nil { - return "", err - } - - return string(data), nil -} - -// ContainerPrune 清理未使用的容器 -func (r *Container) ContainerPrune() error { - _, err := r.client.ContainersPrune(context.Background(), filters.NewArgs()) - return err -} - -// NetworkList 列出网络 -func (r *Container) NetworkList() ([]network.Inspect, error) { - return r.client.NetworkList(context.Background(), network.ListOptions{}) -} - -// NetworkCreate 创建网络 -func (r *Container) NetworkCreate(config requests.NetworkCreate) (string, error) { - var ipamConfigs []network.IPAMConfig - if config.Ipv4.Enabled { - ipamConfigs = append(ipamConfigs, network.IPAMConfig{ - Subnet: config.Ipv4.Subnet, - Gateway: config.Ipv4.Gateway, - IPRange: config.Ipv4.IPRange, - }) - } - if config.Ipv6.Enabled { - ipamConfigs = append(ipamConfigs, network.IPAMConfig{ - Subnet: config.Ipv6.Subnet, - Gateway: config.Ipv6.Gateway, - IPRange: config.Ipv6.IPRange, - }) - } - - options := network.CreateOptions{ - EnableIPv6: &config.Ipv6.Enabled, - Driver: config.Driver, - Options: r.KVToMap(config.Options), - Labels: r.KVToMap(config.Labels), - } - if len(ipamConfigs) > 0 { - options.IPAM = &network.IPAM{ - Config: ipamConfigs, - } - } - - resp, err := r.client.NetworkCreate(context.Background(), config.Name, options) - return resp.ID, err -} - -// NetworkRemove 删除网络 -func (r *Container) NetworkRemove(id string) error { - return r.client.NetworkRemove(context.Background(), id) -} - -// NetworkExist 判断网络是否存在 -func (r *Container) NetworkExist(name string) (bool, error) { - var options network.ListOptions - options.Filters = filters.NewArgs(filters.Arg("name", name)) - networks, err := r.client.NetworkList(context.Background(), options) - if err != nil { - return false, err - } - - return len(networks) > 0, nil -} - -// NetworkInspect 查看网络 -func (r *Container) NetworkInspect(id string) (network.Inspect, error) { - return r.client.NetworkInspect(context.Background(), id, network.InspectOptions{}) -} - -// NetworkConnect 连接网络 -func (r *Container) NetworkConnect(networkID string, containerID string) error { - return r.client.NetworkConnect(context.Background(), networkID, containerID, nil) -} - -// NetworkDisconnect 断开网络 -func (r *Container) NetworkDisconnect(networkID string, containerID string) error { - return r.client.NetworkDisconnect(context.Background(), networkID, containerID, true) -} - -// NetworkPrune 清理未使用的网络 -func (r *Container) NetworkPrune() error { - _, err := r.client.NetworksPrune(context.Background(), filters.NewArgs()) - return err -} - -// ImageList 列出镜像 -func (r *Container) ImageList() ([]image.Summary, error) { - return r.client.ImageList(context.Background(), image.ListOptions{ - All: true, - }) -} - -// ImageExist 判断镜像是否存在 -func (r *Container) ImageExist(id string) (bool, error) { - var options image.ListOptions - options.Filters = filters.NewArgs(filters.Arg("reference", id)) - images, err := r.client.ImageList(context.Background(), options) - if err != nil { - return false, err - } - - return len(images) > 0, nil -} - -// ImagePull 拉取镜像 -func (r *Container) ImagePull(config requests.ImagePull) error { - options := image.PullOptions{} - if config.Auth { - authConfig := registry.AuthConfig{ - Username: config.Username, - Password: config.Password, - } - encodedJSON, err := json.Marshal(authConfig) - if err != nil { - return err - } - authStr := base64.URLEncoding.EncodeToString(encodedJSON) - options.RegistryAuth = authStr - } - - out, err := r.client.ImagePull(context.Background(), config.Name, options) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(io.Discard, out) - return err -} - -// ImageRemove 删除镜像 -func (r *Container) ImageRemove(id string) error { - _, err := r.client.ImageRemove(context.Background(), id, image.RemoveOptions{ - Force: true, - PruneChildren: true, - }) - return err -} - -// ImagePrune 清理未使用的镜像 -func (r *Container) ImagePrune() error { - _, err := r.client.ImagesPrune(context.Background(), filters.NewArgs()) - return err -} - -// ImageInspect 查看镜像 -func (r *Container) ImageInspect(id string) (types.ImageInspect, error) { - img, _, err := r.client.ImageInspectWithRaw(context.Background(), id) - return img, err -} - -// VolumeList 列出存储卷 -func (r *Container) VolumeList() ([]*volume.Volume, error) { - volumes, err := r.client.VolumeList(context.Background(), volume.ListOptions{}) - return volumes.Volumes, err -} - -// VolumeCreate 创建存储卷 -func (r *Container) VolumeCreate(config requests.VolumeCreate) (volume.Volume, error) { - return r.client.VolumeCreate(context.Background(), volume.CreateOptions{ - Name: config.Name, - Driver: config.Driver, - DriverOpts: r.KVToMap(config.Options), - Labels: r.KVToMap(config.Labels), - }) -} - -// VolumeExist 判断存储卷是否存在 -func (r *Container) VolumeExist(id string) (bool, error) { - var options volume.ListOptions - options.Filters = filters.NewArgs(filters.Arg("name", id)) - volumes, err := r.client.VolumeList(context.Background(), options) - if err != nil { - return false, err - } - - return len(volumes.Volumes) > 0, nil -} - -// VolumeInspect 查看存储卷 -func (r *Container) VolumeInspect(id string) (volume.Volume, error) { - return r.client.VolumeInspect(context.Background(), id) -} - -// VolumeRemove 删除存储卷 -func (r *Container) VolumeRemove(id string) error { - return r.client.VolumeRemove(context.Background(), id, true) -} - -// VolumePrune 清理未使用的存储卷 -func (r *Container) VolumePrune() error { - _, err := r.client.VolumesPrune(context.Background(), filters.NewArgs()) - return err -} - -// KVToMap 将 key-value 切片转换为 map -func (r *Container) KVToMap(kvs []paneltypes.KV) map[string]string { - m := make(map[string]string) - for _, item := range kvs { - m[item.Key] = item.Value - } - - return m -} - -// KVToSlice 将 key-value 切片转换为 key=value 切片 -func (r *Container) KVToSlice(kvs []paneltypes.KV) []string { - var s []string - for _, item := range kvs { - s = append(s, item.Key+"="+item.Value) - } - - return s -} diff --git a/internal/services/cron.go b/internal/services/cron.go deleted file mode 100644 index 4233a0a4..00000000 --- a/internal/services/cron.go +++ /dev/null @@ -1,48 +0,0 @@ -package services - -import ( - "errors" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/os" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" -) - -type CronImpl struct { -} - -func NewCronImpl() *CronImpl { - return &CronImpl{} -} - -// AddToSystem 添加到系统 -func (r *CronImpl) AddToSystem(cron models.Cron) error { - if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil { - return err - } - - return r.restartCron() -} - -// DeleteFromSystem 从系统中删除 -func (r *CronImpl) DeleteFromSystem(cron models.Cron) error { - if _, err := shell.Execf(`( crontab -l | grep -v -F "%s %s >> %s 2>&1" ) | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil { - return err - } - - return r.restartCron() -} - -// restartCron 重启 cron 服务 -func (r *CronImpl) restartCron() error { - if os.IsRHEL() { - return systemctl.Restart("crond") - } - - if os.IsDebian() || os.IsUbuntu() { - return systemctl.Restart("cron") - } - - return errors.New("不支持的系统") -} diff --git a/internal/services/php.go b/internal/services/php.go deleted file mode 100644 index 7ea387ab..00000000 --- a/internal/services/php.go +++ /dev/null @@ -1,356 +0,0 @@ -package services - -import ( - "errors" - "fmt" - "regexp" - "slices" - "strings" - "time" - - "github.com/go-resty/resty/v2" - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PHPImpl struct { - version string -} - -func NewPHPImpl(version uint) *PHPImpl { - return &PHPImpl{ - version: cast.ToString(version), - } -} - -func (r *PHPImpl) Reload() error { - return systemctl.Reload("php-fpm-" + r.version) -} - -func (r *PHPImpl) GetConfig() (string, error) { - return io.Read("/www/server/php/" + r.version + "/etc/php.ini") -} - -func (r *PHPImpl) SaveConfig(config string) error { - if err := io.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil { - return err - } - - return r.Reload() -} - -func (r *PHPImpl) GetFPMConfig() (string, error) { - return io.Read("/www/server/php/" + r.version + "/etc/php-fpm.conf") -} - -func (r *PHPImpl) SaveFPMConfig(config string) error { - if err := io.Write("/www/server/php/"+r.version+"/etc/php-fpm.conf", config, 0644); err != nil { - return err - } - - return r.Reload() -} - -func (r *PHPImpl) Load() ([]types.NV, error) { - client := resty.New().SetTimeout(10 * time.Second) - resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version) - if err != nil || !resp.IsSuccess() { - return []types.NV{}, nil - } - - raw := resp.String() - dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"} - regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"} - - data := make([]types.NV, len(dataKeys)) - for i := range dataKeys { - data[i].Name = dataKeys[i] - - r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i])) - match := r.FindStringSubmatch(raw) - - if len(match) > 1 { - data[i].Value = strings.TrimSpace(match[1]) - } - } - - return data, nil -} - -func (r *PHPImpl) GetErrorLog() (string, error) { - return shell.Execf("tail -n 500 /www/server/php/%s/var/log/php-fpm.log", r.version) -} - -func (r *PHPImpl) GetSlowLog() (string, error) { - return shell.Execf("tail -n 500 /www/server/php/%s/var/log/slow.log", r.version) -} - -func (r *PHPImpl) ClearErrorLog() error { - if out, err := shell.Execf("echo '' > /www/server/php/%s/var/log/php-fpm.log", r.version); err != nil { - return errors.New(out) - } - - return r.Reload() -} - -func (r *PHPImpl) ClearSlowLog() error { - if out, err := shell.Execf("echo '' > /www/server/php/%s/var/log/slow.log", r.version); err != nil { - return errors.New(out) - } - - return nil -} - -func (r *PHPImpl) GetExtensions() ([]types.PHPExtension, error) { - extensions := []types.PHPExtension{ - { - Name: "fileinfo", - Slug: "fileinfo", - Description: "Fileinfo 是一个用于识别文件类型的库。", - Installed: false, - }, - { - Name: "OPcache", - Slug: "Zend OPcache", - Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。", - Installed: false, - }, - { - Name: "PhpRedis", - Slug: "redis", - Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据。", - Installed: false, - }, - { - Name: "ImageMagick", - Slug: "imagick", - Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。", - Installed: false, - }, - { - Name: "exif", - Slug: "exif", - Description: "通过 exif 扩展,你可以操作图像元数据。", - Installed: false, - }, - { - Name: "pdo_pgsql", - Slug: "pdo_pgsql", - Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。", - Installed: false, - }, - { - Name: "imap", - Slug: "imap", - Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件。", - Installed: false, - }, - { - Name: "zip", - Slug: "zip", - Description: "Zip 是一个用于处理 ZIP 文件的库。", - Installed: false, - }, - { - Name: "bz2", - Slug: "bz2", - Description: "Bzip2 是一个用于压缩和解压缩文件的库。", - Installed: false, - }, - { - Name: "readline", - Slug: "readline", - Description: "Readline 是一个库,它提供了一种用于处理文本的接口。", - Installed: false, - }, - { - Name: "snmp", - Slug: "snmp", - Description: "SNMP 是一种用于网络管理的协议。", - Installed: false, - }, - { - Name: "ldap", - Slug: "ldap", - Description: "LDAP 是一种用于访问目录服务的协议。", - }, - { - Name: "enchant", - Slug: "enchant", - Description: "Enchant 是一个拼写检查库。", - Installed: false, - }, - { - Name: "pspell", - Slug: "pspell", - Description: "Pspell 是一个拼写检查库。", - Installed: false, - }, - { - Name: "calendar", - Slug: "calendar", - Description: "Calendar 是一个用于处理日期的库。", - Installed: false, - }, - { - Name: "gmp", - Slug: "gmp", - Description: "GMP 是一个用于处理大整数的库。", - Installed: false, - }, - { - Name: "sysvmsg", - Slug: "sysvmsg", - Description: "Sysvmsg 是一个用于处理 System V 消息队列的库。", - Installed: false, - }, - { - Name: "sysvsem", - Slug: "sysvsem", - Description: "Sysvsem 是一个用于处理 System V 信号量的库。", - }, - { - Name: "sysvshm", - Slug: "sysvshm", - Description: "Sysvshm 是一个用于处理 System V 共享内存的库。", - Installed: false, - }, - { - Name: "xsl", - Slug: "xsl", - Description: "XSL 是一个用于处理 XML 文档的库。", - Installed: false, - }, - { - Name: "intl", - Slug: "intl", - Description: "Intl 是一个用于处理国际化和本地化的库。", - Installed: false, - }, - { - Name: "gettext", - Slug: "gettext", - Description: "Gettext 是一个用于处理多语言的库。", - Installed: false, - }, - { - Name: "igbinary", - Slug: "igbinary", - Description: "Igbinary 是一个用于序列化和反序列化数据的库。", - Installed: false, - }, - } - - // ionCube 只支持 PHP 8.3 以下版本 - if cast.ToUint(r.version) < 83 { - extensions = append(extensions, types.PHPExtension{ - Name: "ionCube", - Slug: "ionCube Loader", - Description: "ionCube 是一个专业级的 PHP 加密解密工具。", - Installed: false, - }) - } - // Swoole 和 Swow 不支持 PHP 8.0 以下版本 - if cast.ToUint(r.version) >= 80 { - extensions = append(extensions, types.PHPExtension{ - Name: "Swoole", - Slug: "swoole", - Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", - Installed: false, - }) - extensions = append(extensions, types.PHPExtension{ - Name: "Swow", - Slug: "Swow", - Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。", - Installed: false, - }) - } - - raw, err := shell.Execf("/www/server/php/%s/bin/php -m", r.version) - if err != nil { - return extensions, err - } - - extensionMap := make(map[string]*types.PHPExtension) - for i := range extensions { - extensionMap[extensions[i].Slug] = &extensions[i] - } - - rawExtensionList := strings.Split(raw, "\n") - for _, item := range rawExtensionList { - if ext, exists := extensionMap[item]; exists && !strings.Contains(item, "[") && item != "" { - ext.Installed = true - } - } - - return extensions, nil -} - -func (r *PHPImpl) InstallExtension(slug string) error { - if !r.checkExtension(slug) { - return errors.New("扩展不存在") - } - - shell := fmt.Sprintf(`bash '/www/panel/scripts/php_extensions/%s.sh' install %s >> '/tmp/%s.log' 2>&1`, slug, r.version, slug) - - officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"} - if slices.Contains(officials, slug) { - shell = fmt.Sprintf(`bash '/www/panel/scripts/php_extensions/official.sh' install '%s' '%s' >> '/tmp/%s.log' 2>&1`, r.version, slug, slug) - } - - var task models.Task - task.Name = "安装PHP-" + r.version + "扩展-" + slug - task.Status = models.TaskStatusWaiting - task.Shell = shell - task.Log = "/tmp/" + slug + ".log" - if err := facades.Orm().Query().Create(&task); err != nil { - return err - } - - return NewTaskImpl().Process(task.ID) -} - -func (r *PHPImpl) UninstallExtension(slug string) error { - if !r.checkExtension(slug) { - return errors.New("扩展不存在") - } - - shell := fmt.Sprintf(`bash '/www/panel/scripts/php_extensions/%s.sh' uninstall %s >> '/tmp/%s.log' 2>&1`, slug, r.version, slug) - - officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"} - if slices.Contains(officials, slug) { - shell = fmt.Sprintf(`bash '/www/panel/scripts/php_extensions/official.sh' uninstall '%s' '%s' >> '/tmp/%s.log' 2>&1`, r.version, slug, slug) - } - - var task models.Task - task.Name = "卸载PHP-" + r.version + "扩展-" + slug - task.Status = models.TaskStatusWaiting - task.Shell = shell - task.Log = "/tmp/" + slug + ".log" - if err := facades.Orm().Query().Create(&task); err != nil { - return err - } - - return NewTaskImpl().Process(task.ID) -} - -func (r *PHPImpl) checkExtension(slug string) bool { - extensions, err := r.GetExtensions() - if err != nil { - return false - } - - for _, item := range extensions { - if item.Slug == slug { - return true - } - } - - return false -} diff --git a/internal/services/plugin.go b/internal/services/plugin.go deleted file mode 100644 index 9ea0f3a1..00000000 --- a/internal/services/plugin.go +++ /dev/null @@ -1,247 +0,0 @@ -// Package services 插件服务 -package services - -import ( - "errors" - - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/app/plugins/loader" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type PluginImpl struct { - task internal.Task -} - -func NewPluginImpl() *PluginImpl { - return &PluginImpl{ - task: NewTaskImpl(), - } -} - -// AllInstalled 获取已安装的所有插件 -func (r *PluginImpl) AllInstalled() ([]models.Plugin, error) { - var plugins []models.Plugin - if err := facades.Orm().Query().Get(&plugins); err != nil { - return plugins, err - } - - return plugins, nil -} - -// All 获取所有插件 -func (r *PluginImpl) All() []*types.Plugin { - var _ = []types.Plugin{ - types.PluginOpenResty, - types.PluginMySQL57, - types.PluginMySQL80, - types.PluginMySQL84, - types.PluginPostgreSQL15, - types.PluginPostgreSQL16, - types.PluginPHP74, - types.PluginPHP80, - types.PluginPHP81, - types.PluginPHP82, - types.PluginPHP83, - types.PluginPHPMyAdmin, - types.PluginPureFTPd, - types.PluginRedis, - types.PluginS3fs, - types.PluginRsync, - types.PluginSupervisor, - types.PluginFail2ban, - types.PluginPodman, - types.PluginFrp, - types.PluginGitea, - types.PluginToolBox, - } - - return loader.All() -} - -// GetBySlug 根据 slug 获取插件 -func (r *PluginImpl) GetBySlug(slug string) *types.Plugin { - for _, item := range r.All() { - if item.Slug == slug { - return item - } - } - - return &types.Plugin{} -} - -// GetInstalledBySlug 根据 slug 获取已安装的插件 -func (r *PluginImpl) GetInstalledBySlug(slug string) models.Plugin { - var plugin models.Plugin - _ = facades.Orm().Query().Where("slug", slug).Get(&plugin) - return plugin -} - -// Install 安装插件 -func (r *PluginImpl) Install(slug string) error { - plugin := r.GetBySlug(slug) - installedPlugin := r.GetInstalledBySlug(slug) - installedPlugins, err := r.AllInstalled() - if err != nil { - return err - } - - if installedPlugin.ID != 0 { - return errors.New("插件已安装") - } - - pluginsMap := make(map[string]bool) - - for _, p := range installedPlugins { - pluginsMap[p.Slug] = true - } - - for _, require := range plugin.Requires { - _, requireFound := pluginsMap[require] - if !requireFound { - return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") - } - } - - for _, exclude := range plugin.Excludes { - _, excludeFound := pluginsMap[exclude] - if excludeFound { - return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") - } - } - - if err = r.checkTaskExists(slug); err != nil { - return err - } - - var task models.Task - task.Name = "安装插件 " + plugin.Name - task.Status = models.TaskStatusWaiting - task.Shell = plugin.Install + ` >> '/tmp/` + plugin.Slug + `.log' 2>&1` - task.Log = "/tmp/" + plugin.Slug + ".log" - if err = facades.Orm().Query().Create(&task); err != nil { - return errors.New("创建任务失败") - } - - _ = io.Remove(task.Log) - return r.task.Process(task.ID) -} - -// Uninstall 卸载插件 -func (r *PluginImpl) Uninstall(slug string) error { - plugin := r.GetBySlug(slug) - installedPlugin := r.GetInstalledBySlug(slug) - installedPlugins, err := r.AllInstalled() - if err != nil { - return err - } - - if installedPlugin.ID == 0 { - return errors.New("插件未安装") - } - - pluginsMap := make(map[string]bool) - - for _, p := range installedPlugins { - pluginsMap[p.Slug] = true - } - - for _, require := range plugin.Requires { - _, requireFound := pluginsMap[require] - if !requireFound { - return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") - } - } - - for _, exclude := range plugin.Excludes { - _, excludeFound := pluginsMap[exclude] - if excludeFound { - return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") - } - } - - if err = r.checkTaskExists(slug); err != nil { - return err - } - - var task models.Task - task.Name = "卸载插件 " + plugin.Name - task.Status = models.TaskStatusWaiting - task.Shell = plugin.Uninstall + " >> /tmp/" + plugin.Slug + ".log 2>&1" - task.Log = "/tmp/" + plugin.Slug + ".log" - if err = facades.Orm().Query().Create(&task); err != nil { - return errors.New("创建任务失败") - } - - _ = io.Remove(task.Log) - return r.task.Process(task.ID) -} - -// Update 更新插件 -func (r *PluginImpl) Update(slug string) error { - plugin := r.GetBySlug(slug) - installedPlugin := r.GetInstalledBySlug(slug) - installedPlugins, err := r.AllInstalled() - if err != nil { - return err - } - - if installedPlugin.ID == 0 { - return errors.New("插件未安装") - } - - pluginsMap := make(map[string]bool) - - for _, p := range installedPlugins { - pluginsMap[p.Slug] = true - } - - for _, require := range plugin.Requires { - _, requireFound := pluginsMap[require] - if !requireFound { - return errors.New("插件 " + slug + " 需要依赖 " + require + " 插件") - } - } - - for _, exclude := range plugin.Excludes { - _, excludeFound := pluginsMap[exclude] - if excludeFound { - return errors.New("插件 " + slug + " 不兼容 " + exclude + " 插件") - } - } - - if err = r.checkTaskExists(slug); err != nil { - return err - } - - var task models.Task - task.Name = "更新插件 " + plugin.Name - task.Status = models.TaskStatusWaiting - task.Shell = plugin.Update + " >> /tmp/" + plugin.Slug + ".log 2>&1" - task.Log = "/tmp/" + plugin.Slug + ".log" - if err = facades.Orm().Query().Create(&task); err != nil { - return errors.New("创建任务失败") - } - - _ = io.Remove(task.Log) - return r.task.Process(task.ID) -} - -func (r *PluginImpl) checkTaskExists(slug string) error { - var count int64 - if err := facades.Orm().Query(). - Model(&models.Task{}). - Where("log LIKE ? AND (status = ? OR status = ?)", "%"+slug+"%", models.TaskStatusWaiting, models.TaskStatusRunning). - Count(&count); err != nil { - return errors.New("查询任务失败") - } - if count > 0 { - return errors.New("任务已添加,请勿重复添加") - } - - return nil -} diff --git a/internal/services/setting.go b/internal/services/setting.go deleted file mode 100644 index 53889871..00000000 --- a/internal/services/setting.go +++ /dev/null @@ -1,50 +0,0 @@ -// Package services 设置服务 -package services - -import ( - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/str" -) - -type SettingImpl struct { -} - -func NewSettingImpl() *SettingImpl { - return &SettingImpl{} -} - -// Get 获取设置 -func (r *SettingImpl) Get(key string, defaultValue ...string) string { - var setting models.Setting - if err := facades.Orm().Query().Where("key", key).FirstOrFail(&setting); err != nil { - return str.FirstElement(defaultValue) - } - - if len(setting.Value) == 0 { - return str.FirstElement(defaultValue) - } - - return setting.Value -} - -// Set 更新或创建设置 -func (r *SettingImpl) Set(key, value string) error { - var setting models.Setting - if err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{Key: key}, models.Setting{Value: value}); err != nil { - return err - } - - return nil -} - -// Delete 删除设置 -func (r *SettingImpl) Delete(key string) error { - var setting models.Setting - if _, err := facades.Orm().Query().Where("key", key).Delete(&setting); err != nil { - return err - } - - return nil -} diff --git a/internal/services/task.go b/internal/services/task.go deleted file mode 100644 index f36f13af..00000000 --- a/internal/services/task.go +++ /dev/null @@ -1,42 +0,0 @@ -package services - -import ( - "sync" - - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/jobs" - "github.com/TheTNB/panel/v2/app/models" -) - -var taskMap sync.Map - -type TaskImpl struct { -} - -func NewTaskImpl() *TaskImpl { - return &TaskImpl{} -} - -func (r *TaskImpl) Process(taskID uint) error { - taskMap.Store(taskID, true) - return facades.Queue().Job(&jobs.ProcessTask{}, []any{taskID}).Dispatch() -} - -func (r *TaskImpl) DispatchWaiting() error { - var tasks []models.Task - if err := facades.Orm().Query().Where("status = ?", models.TaskStatusWaiting).Find(&tasks); err != nil { - return err - } - - for _, task := range tasks { - if _, ok := taskMap.Load(task.ID); ok { - continue - } - if err := r.Process(task.ID); err != nil { - return err - } - } - - return nil -} diff --git a/internal/services/user.go b/internal/services/user.go deleted file mode 100644 index a42764bd..00000000 --- a/internal/services/user.go +++ /dev/null @@ -1,35 +0,0 @@ -// Package services 用户服务 -package services - -import ( - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/models" -) - -type UserImpl struct { -} - -func NewUserImpl() *UserImpl { - return &UserImpl{} -} - -func (r *UserImpl) Create(username, password string) (models.User, error) { - user := models.User{ - Username: username, - Password: password, - } - if err := facades.Orm().Query().Create(&user); err != nil { - return user, err - } - - return user, nil -} - -func (r *UserImpl) Update(user models.User) (models.User, error) { - if _, err := facades.Orm().Query().Update(&user); err != nil { - return user, err - } - - return user, nil -} diff --git a/internal/services/website.go b/internal/services/website.go deleted file mode 100644 index dfa2d43d..00000000 --- a/internal/services/website.go +++ /dev/null @@ -1,630 +0,0 @@ -// Package services 网站服务 -package services - -import ( - "errors" - "fmt" - "path/filepath" - "regexp" - "slices" - "strconv" - "strings" - - "github.com/goravel/framework/facades" - "github.com/spf13/cast" - - requests "github.com/TheTNB/panel/v2/app/http/requests/website" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/embed" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/pkg/cert" - "github.com/TheTNB/panel/v2/pkg/db" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/str" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type WebsiteImpl struct { - setting internal.Setting -} - -func NewWebsiteImpl() *WebsiteImpl { - return &WebsiteImpl{ - setting: NewSettingImpl(), - } -} - -// List 列出网站 -func (r *WebsiteImpl) List(page, limit int) (int64, []models.Website, error) { - var websites []models.Website - var total int64 - if err := facades.Orm().Query().Paginate(page, limit, &websites, &total); err != nil { - return total, websites, err - } - - return total, websites, nil -} - -// Add 添加网站 -func (r *WebsiteImpl) Add(website requests.Add) (models.Website, error) { - w := models.Website{ - Name: website.Name, - Status: true, - Path: website.Path, - PHP: cast.ToInt(website.PHP), - SSL: false, - } - if err := facades.Orm().Query().Create(&w); err != nil { - return models.Website{}, err - } - - if err := io.Mkdir(website.Path, 0755); err != nil { - return models.Website{}, err - } - - index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html")) - if err != nil { - return models.Website{}, fmt.Errorf("获取index模板文件失败: %w", err) - } - if err = io.Write(website.Path+"/index.html", string(index), 0644); err != nil { - return models.Website{}, err - } - - notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html")) - if err != nil { - return models.Website{}, fmt.Errorf("获取404模板文件失败: %w", err) - } - if err = io.Write(website.Path+"/404.html", string(notFound), 0644); err != nil { - return models.Website{}, err - } - - portList := "" - domainList := "" - portUsed := make(map[uint]bool) - domainUsed := make(map[string]bool) - - for i, port := range website.Ports { - if _, ok := portUsed[port]; !ok { - if i == len(website.Ports)-1 { - portList += " listen " + cast.ToString(port) + ";\n" - portList += " listen [::]:" + cast.ToString(port) + ";" - } else { - portList += " listen " + cast.ToString(port) + ";\n" - portList += " listen [::]:" + cast.ToString(port) + ";\n" - } - portUsed[port] = true - } - } - for _, domain := range website.Domains { - if _, ok := domainUsed[domain]; !ok { - domainList += " " + domain - domainUsed[domain] = true - } - } - - nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! -# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 -server -{ - # port标记位开始 -%s - # port标记位结束 - # server_name标记位开始 - server_name%s; - # server_name标记位结束 - # index标记位开始 - index index.php index.html; - # index标记位结束 - # root标记位开始 - root %s; - # root标记位结束 - - # ssl标记位开始 - # ssl标记位结束 - - # php标记位开始 - include enable-php-%s.conf; - # php标记位结束 - - # waf标记位开始 - waf off; - waf_rule_path /www/server/openresty/ngx_waf/assets/rules/; - waf_mode DYNAMIC; - waf_cc_deny rate=1000r/m duration=60m; - waf_cache capacity=50; - # waf标记位结束 - - # 错误页配置,可自行设置 - error_page 404 /404.html; - #error_page 502 /502.html; - - # acme证书签发配置,不可修改 - include /www/server/vhost/acme/%s.conf; - - # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 - include /www/server/vhost/rewrite/%s.conf; - - # 面板默认禁止访问部分敏感目录,可自行修改 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) - { - return 404; - } - # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 - location ~ .*\.(js|css)$ - { - expires 1h; - error_log /dev/null; - access_log /dev/null; - } - - access_log /www/wwwlogs/%s.log; - error_log /www/wwwlogs/%s.log; -} -`, portList, domainList, website.Path, website.PHP, website.Name, website.Name, website.Name, website.Name) - - if err = io.Write("/www/server/vhost/"+website.Name+".conf", nginxConf, 0644); err != nil { - return models.Website{}, err - } - if err = io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644); err != nil { - return models.Website{}, err - } - if err = io.Write("/www/server/vhost/acme/"+website.Name+".conf", "", 0644); err != nil { - return models.Website{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".pem", "", 0644); err != nil { - return models.Website{}, err - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".key", "", 0644); err != nil { - return models.Website{}, err - } - - if err = io.Chmod(website.Path, 0755); err != nil { - return models.Website{}, err - } - if err = io.Chown(website.Path, "www", "www"); err != nil { - return models.Website{}, err - } - - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return models.Website{}, err - } - - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - if website.DB && website.DBType == "mysql" { - mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") - if err != nil { - return models.Website{}, err - } - if err = mysql.DatabaseCreate(website.DBName); err != nil { - return models.Website{}, err - } - if err = mysql.UserCreate(website.DBUser, website.DBPassword); err != nil { - return models.Website{}, err - } - if err = mysql.PrivilegesGrant(website.DBUser, website.DBName); err != nil { - return models.Website{}, err - } - } - if website.DB && website.DBType == "postgresql" { - _, _ = shell.Execf(`echo "CREATE DATABASE '%s';" | su - postgres -c "psql"`, website.DBName) - _, _ = shell.Execf(`echo "CREATE USER '%s' WITH PASSWORD '%s';" | su - postgres -c "psql"`, website.DBUser, website.DBPassword) - _, _ = shell.Execf(`echo "ALTER DATABASE '%s' OWNER TO '%s';" | su - postgres -c "psql"`, website.DBName, website.DBUser) - _, _ = shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE '%s' TO '%s';" | su - postgres -c "psql"`, website.DBName, website.DBUser) - userConfig := "host " + website.DBName + " " + website.DBUser + " 127.0.0.1/32 scram-sha-256" - _, _ = shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`) - _ = systemctl.Reload("postgresql") - } - - return w, nil -} - -// SaveConfig 保存网站配置 -func (r *WebsiteImpl) SaveConfig(config requests.SaveConfig) error { - var website models.Website - if err := facades.Orm().Query().Where("id", config.ID).First(&website); err != nil { - return err - } - - if !website.Status { - return errors.New("网站已停用,请先启用") - } - - // 原文 - raw, err := io.Read("/www/server/vhost/" + website.Name + ".conf") - if err != nil { - return err - } - if strings.TrimSpace(raw) != strings.TrimSpace(config.Raw) { - if err = io.Write("/www/server/vhost/"+website.Name+".conf", config.Raw, 0644); err != nil { - return err - } - if err = systemctl.Reload("openresty"); err != nil { - _, err = shell.Execf("openresty -t") - return err - } - - return nil - } - - // 目录 - path := config.Path - if !io.Exists(path) { - return errors.New("网站目录不存在") - } - website.Path = path - - // 域名 - domain := "server_name" - domains := config.Domains - for _, v := range domains { - if v == "" { - continue - } - domain += " " + v - } - domain += ";" - domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束") - if len(strings.TrimSpace(domainConfigOld)) == 0 { - return errors.New("配置文件中缺少server_name标记位") - } - raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1) - - // 端口 - var portConf strings.Builder - ports := config.Ports - for _, port := range ports { - https := "" - quic := false - if slices.Contains(config.SSLPorts, port) { - https = " ssl" - if slices.Contains(config.QUICPorts, port) { - quic = true - } - } - - portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https)) - portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https)) - if quic { - portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic")) - portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic")) - } - } - portConf.WriteString(" ") - portConfNew := portConf.String() - portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束") - if len(strings.TrimSpace(portConfOld)) == 0 { - return errors.New("配置文件中缺少port标记位") - } - raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1) - - // 运行目录 - root := str.Cut(raw, "# root标记位开始", "# root标记位结束") - if len(strings.TrimSpace(root)) == 0 { - return errors.New("配置文件中缺少root标记位") - } - match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root) - if len(match) != 2 { - return errors.New("配置文件中root标记位格式错误") - } - rootNew := strings.Replace(root, match[1], config.Root, -1) - raw = strings.Replace(raw, root, rootNew, -1) - - // 默认文件 - index := str.Cut(raw, "# index标记位开始", "# index标记位结束") - if len(strings.TrimSpace(index)) == 0 { - return errors.New("配置文件中缺少index标记位") - } - match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index) - if len(match) != 2 { - return errors.New("配置文件中index标记位格式错误") - } - indexNew := strings.Replace(index, match[1], config.Index, -1) - raw = strings.Replace(raw, index, indexNew, -1) - - // 防跨站 - root = config.Root - if !strings.HasSuffix(root, "/") { - root += "/" - } - if config.OpenBasedir { - if err = io.Write(root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil { - return err - } - } else { - if io.Exists(root + ".user.ini") { - if err = io.Remove(root + ".user.ini"); err != nil { - return err - } - } - } - - // WAF - waf := config.Waf - wafStr := "off" - if waf { - wafStr = "on" - } - wafMode := config.WafMode - wafCcDeny := config.WafCcDeny - wafCache := config.WafCache - wafConfig := `# waf标记位开始 - waf ` + wafStr + `; - waf_rule_path /www/server/openresty/ngx_waf/assets/rules/; - waf_mode ` + wafMode + `; - waf_cc_deny ` + wafCcDeny + `; - waf_cache ` + wafCache + `; - ` - wafConfigOld := str.Cut(raw, "# waf标记位开始", "# waf标记位结束") - if len(strings.TrimSpace(wafConfigOld)) != 0 { - raw = strings.Replace(raw, wafConfigOld, "", -1) - } - raw = strings.Replace(raw, "# waf标记位开始", wafConfig, -1) - - // SSL - ssl := config.SSL - website.SSL = ssl - if ssl { - if _, err = cert.ParseCert(config.SSLCertificate); err != nil { - return errors.New("TLS证书格式错误") - } - if _, err = cert.ParseKey(config.SSLCertificateKey); err != nil { - return errors.New("TLS私钥格式错误") - } - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".pem", config.SSLCertificate, 0644); err != nil { - return err - } - if err = io.Write("/www/server/vhost/ssl/"+website.Name+".key", config.SSLCertificateKey, 0644); err != nil { - return err - } - if ssl { - sslConfig := `# ssl标记位开始 - ssl_certificate /www/server/vhost/ssl/` + website.Name + `.pem; - ssl_certificate_key /www/server/vhost/ssl/` + website.Name + `.key; - ssl_session_timeout 1d; - ssl_session_cache shared:SSL:10m; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; - ssl_prefer_server_ciphers off; - ssl_early_data on; - ` - if config.HTTPRedirect { - sslConfig += `# http重定向标记位开始 - if ($server_port !~ 443){ - return 301 https://$host$request_uri; - } - error_page 497 https://$host$request_uri; - # http重定向标记位结束 - ` - } - if config.HSTS { - sslConfig += `# hsts标记位开始 - add_header Strict-Transport-Security "max-age=63072000" always; - # hsts标记位结束 - ` - } - if config.OCSP { - sslConfig += `# ocsp标记位开始 - ssl_stapling on; - ssl_stapling_verify on; - # ocsp标记位结束 - ` - } - sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") - if len(strings.TrimSpace(sslConfigOld)) != 0 { - raw = strings.Replace(raw, sslConfigOld, "", -1) - } - raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1) - } else { - sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束") - if len(strings.TrimSpace(sslConfigOld)) != 0 { - raw = strings.Replace(raw, sslConfigOld, "\n ", -1) - } - } - - if website.PHP != config.PHP { - website.PHP = config.PHP - phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束") - phpConfig := ` - include enable-php-` + strconv.Itoa(website.PHP) + `.conf; - ` - if len(strings.TrimSpace(phpConfigOld)) != 0 { - raw = strings.Replace(raw, phpConfigOld, phpConfig, -1) - } - } - - if err = facades.Orm().Query().Save(&website); err != nil { - return err - } - - if err = io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil { - return err - } - if err = io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", config.Rewrite, 0644); err != nil { - return err - } - - err = systemctl.Reload("openresty") - if err != nil { - _, err = shell.Execf("openresty -t") - } - - return err -} - -// Delete 删除网站 -func (r *WebsiteImpl) Delete(request requests.Delete) error { - var website models.Website - if err := facades.Orm().Query().With("Cert").Where("id", request.ID).FirstOrFail(&website); err != nil { - return err - } - - if website.Cert != nil { - return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书") - } - - if _, err := facades.Orm().Query().Delete(&website); err != nil { - return err - } - - _ = io.Remove("/www/server/vhost/" + website.Name + ".conf") - _ = io.Remove("/www/server/vhost/rewrite/" + website.Name + ".conf") - _ = io.Remove("/www/server/vhost/acme/" + website.Name + ".conf") - _ = io.Remove("/www/server/vhost/ssl/" + website.Name + ".pem") - _ = io.Remove("/www/server/vhost/ssl/" + website.Name + ".key") - - if request.Path { - _ = io.Remove(website.Path) - } - if request.DB { - rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) - mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix") - if err != nil { - return err - } - _ = mysql.DatabaseDrop(website.Name) - _ = mysql.UserDrop(website.Name) - _, _ = shell.Execf(`echo "DROP DATABASE IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) - _, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name) - } - - err := systemctl.Reload("openresty") - if err != nil { - _, err = shell.Execf("openresty -t") - } - - return err -} - -// GetConfig 获取网站配置 -func (r *WebsiteImpl) GetConfig(id uint) (types.WebsiteSetting, error) { - var website models.Website - if err := facades.Orm().Query().Where("id", id).First(&website); err != nil { - return types.WebsiteSetting{}, err - } - - config, err := io.Read("/www/server/vhost/" + website.Name + ".conf") - if err != nil { - return types.WebsiteSetting{}, err - } - - var setting types.WebsiteSetting - setting.Name = website.Name - setting.Path = website.Path - setting.SSL = website.SSL - setting.PHP = strconv.Itoa(website.PHP) - setting.Raw = config - - portStr := str.Cut(config, "# port标记位开始", "# port标记位结束") - matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1) - for _, match := range matches { - if len(match) < 2 { - continue - } - // 跳过 ipv6 - if strings.Contains(match[1], "[::]") { - continue - } - - // 处理 443 ssl 之类的情况 - ports := strings.Fields(match[1]) - if len(ports) == 0 { - continue - } - if !slices.Contains(setting.Ports, ports[0]) { - setting.Ports = append(setting.Ports, ports[0]) - } - if len(ports) > 1 && ports[1] == "ssl" { - setting.SSLPorts = append(setting.SSLPorts, ports[0]) - } else if len(ports) > 1 && ports[1] == "quic" { - setting.QUICPorts = append(setting.QUICPorts, ports[0]) - } - } - serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束") - match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName) - if len(match) > 1 { - setting.Domains = strings.Split(match[1], " ") - } - root := str.Cut(config, "# root标记位开始", "# root标记位结束") - match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root) - if len(match) > 1 { - setting.Root = match[1] - } - index := str.Cut(config, "# index标记位开始", "# index标记位结束") - match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index) - if len(match) > 1 { - setting.Index = match[1] - } - - if io.Exists(filepath.Join(setting.Root, ".user.ini")) { - userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini")) - if strings.Contains(userIni, "open_basedir") { - setting.OpenBasedir = true - } - } - - crt, _ := io.Read("/www/server/vhost/ssl/" + website.Name + ".pem") - setting.SSLCertificate = crt - key, _ := io.Read("/www/server/vhost/ssl/" + website.Name + ".key") - setting.SSLCertificateKey = key - if setting.SSL { - ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束") - setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位") - setting.HSTS = strings.Contains(ssl, "# hsts标记位") - setting.OCSP = strings.Contains(ssl, "# ocsp标记位") - } - - // 解析证书信息 - if decode, err := cert.ParseCert(crt); err == nil { - setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05") - setting.SSLNotAfter = decode.NotAfter.Format("2006-01-02 15:04:05") - setting.SSLIssuer = decode.Issuer.CommonName - setting.SSLOCSPServer = decode.OCSPServer - setting.SSLDNSNames = decode.DNSNames - } - - waf := str.Cut(config, "# waf标记位开始", "# waf标记位结束") - setting.Waf = strings.Contains(waf, "waf on;") - match = regexp.MustCompile(`waf_mode\s+([^;]*);?`).FindStringSubmatch(waf) - if len(match) > 1 { - setting.WafMode = match[1] - } - match = regexp.MustCompile(`waf_cc_deny\s+([^;]*);?`).FindStringSubmatch(waf) - if len(match) > 1 { - setting.WafCcDeny = match[1] - } - match = regexp.MustCompile(`waf_cache\s+([^;]*);?`).FindStringSubmatch(waf) - if len(match) > 1 { - setting.WafCache = match[1] - } - - rewrite, _ := io.Read("/www/server/vhost/rewrite/" + website.Name + ".conf") - setting.Rewrite = rewrite - log, _ := shell.Execf(`tail -n 100 '/www/wwwlogs/%s.log'`, website.Name) - setting.Log = log - - return setting, err -} - -// GetConfigByName 根据网站名称获取网站配置 -func (r *WebsiteImpl) GetConfigByName(name string) (types.WebsiteSetting, error) { - var website models.Website - if err := facades.Orm().Query().Where("name", name).First(&website); err != nil { - return types.WebsiteSetting{}, err - } - - return r.GetConfig(website.ID) -} - -// GetIDByName 根据网站名称获取网站ID -func (r *WebsiteImpl) GetIDByName(name string) (uint, error) { - var website models.Website - if err := facades.Orm().Query().Where("name", name).First(&website); err != nil { - return 0, err - } - - return website.ID, nil -} diff --git a/internal/setting.go b/internal/setting.go deleted file mode 100644 index 552063ed..00000000 --- a/internal/setting.go +++ /dev/null @@ -1,7 +0,0 @@ -package internal - -type Setting interface { - Get(key string, defaultValue ...string) string - Set(key, value string) error - Delete(key string) error -} diff --git a/internal/task.go b/internal/task.go deleted file mode 100644 index 8a2747d7..00000000 --- a/internal/task.go +++ /dev/null @@ -1,6 +0,0 @@ -package internal - -type Task interface { - Process(taskID uint) error - DispatchWaiting() error -} diff --git a/internal/user.go b/internal/user.go deleted file mode 100644 index f0c47257..00000000 --- a/internal/user.go +++ /dev/null @@ -1,8 +0,0 @@ -package internal - -import "github.com/TheTNB/panel/v2/app/models" - -type User interface { - Create(name, password string) (models.User, error) - Update(user models.User) (models.User, error) -} diff --git a/internal/website.go b/internal/website.go deleted file mode 100644 index c654262f..00000000 --- a/internal/website.go +++ /dev/null @@ -1,17 +0,0 @@ -package internal - -import ( - requests "github.com/TheTNB/panel/v2/app/http/requests/website" - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/pkg/types" -) - -type Website interface { - List(page int, limit int) (int64, []models.Website, error) - Add(website requests.Add) (models.Website, error) - SaveConfig(config requests.SaveConfig) error - Delete(id requests.Delete) error - GetConfig(id uint) (types.WebsiteSetting, error) - GetConfigByName(name string) (types.WebsiteSetting, error) - GetIDByName(name string) (uint, error) -} diff --git a/lang/en.json b/lang/en.json deleted file mode 100644 index af8db26f..00000000 --- a/lang/en.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "auth": { - "session": { - "expired": "session has expired", - "missing": "Please enable cookies and try again", - "invalid": "invalid session" - } - }, - "commands": { - "panel:cert-renew": { - "description": "[Panel] Certificate renewal" - }, - "panel:monitoring": { - "description": "[Panel] System Monitoring", - "fail": "[Panel] System monitoring failed to save" - }, - "panel": { - "description": "[Panel] Command line", - "forDeveloper": "Please use the following commands under the guidance of the developer", - "use": "Please use the following commands", - "tool": "command line tool", - "portFail": "failed to get panel port", - "port": "Panel port", - "entrance": "Panel entrance", - "update": { - "description": "update/fix panel to latest version", - "taskCheck": "There is currently a task being executed and updates are prohibited.", - "dbFail": "panel database exception, operation terminated", - "versionFail": "failed to get latest version", - "fail": "update failed", - "success": "update completed" - }, - "getInfo": { - "description": "reinitialize panel account information", - "adminGetFail": "failed to get administrator information", - "adminSaveFail": "failed to save administrator information", - "passwordGenerationFail": "failed to generate password", - "username": "Username", - "password": "Password", - "address": "Panel address" - }, - "getPort": { - "description": "get the panel access port" - }, - "getEntrance": { - "description": "get panel access" - }, - "deleteEntrance": { - "description": "delete panel access", - "fail": "failed to delete panel entrance", - "success": "panel entrance deleted successfully" - }, - "cleanTask": { - "description": "clean up running and waiting tasks in the panel [used when tasks are stuck]", - "fail": "failed to clean up tasks", - "success": "tasks cleaned up successfully" - }, - "backup": { - "description": "back up website/MySQL database/PostgreSQL database to the specified directory and retain the specified amount", - "paramFail": "backup type, path, name and keep amount are required", - "start": "start backup", - "backupDirFail": "failed to create backup directory", - "targetSite": "target website", - "siteNotExist": "website does not exist", - "backupFail": "backup failed", - "backupSuccess": "backup successful", - "mysqlBackupFail": "MySQL database backup failed", - "targetMysql": "target MySQL database", - "startExport": "start exporting", - "exportFail": "export failed", - "exportSuccess": "export successful", - "startCompress": "start compressing", - "compressFail": "compression failed", - "compressSuccess": "compression successful", - "startMove": "start moving", - "moveFail": "move failed", - "moveSuccess": "move successful", - "databaseGetFail": "failed to get database", - "databaseNotExist": "database does not exist", - "targetPostgres": "target PostgreSQL database", - "cleanBackup": "clean backup", - "cleanupFail": "cleanup failed", - "cleanupSuccess": "cleanup successful", - "deleteFail": "failed to delete", - "success": "backup completed" - }, - "cutoff": { - "description": "cut website logs and keep specified amount", - "paramFail": "website domain name and keep amount are required", - "start": "start cutting", - "targetSite": "target website", - "siteNotExist": "website does not exist", - "logNotExist": "log file does not exist", - "backupFail": "backup failed", - "clearFail": "clearing failed", - "cleanupFail": "cleanup failed", - "clearLog": "clear log", - "cutSuccess": "cutting successful", - "cleanupSuccess": "cleanup successful", - "end": "cutting completed" - }, - "installPlugin": { - "description": "install plugin", - "paramFail": "plugin slug is required", - "success": "task has been submitted" - }, - "uninstallPlugin": { - "description": "uninstall plugin", - "paramFail": "plugin slug is required", - "success": "task has been submitted" - }, - "updatePlugin": { - "description": "update plugin", - "paramFail": "plugin slug is required", - "success": "task has been submitted" - }, - "addSite": { - "description": "add website [domain name and port separated by commas]", - "paramFail": "name, domain, port and path are required", - "siteExist": "website already exists", - "success": "website added successfully" - }, - "removeSite": { - "description": "remove website", - "paramFail": "name is required", - "siteNotExist": "website does not exist", - "success": "website deleted successfully" - }, - "init": { - "description": "initialize the panel", - "exist": "panel has been initialized", - "success": "initialization successful", - "adminFound": "failed to create administrator", - "fail": "initialization failed" - }, - "writePlugin": { - "description": "write plugin installation status", - "paramFail": "plugin slug and version are required", - "fail": "failed to write plugin installation status", - "success": "plugin installation status written successfully" - }, - "deletePlugin": { - "description": "delete plugin installation status", - "paramFail": "plugin slug is required", - "fail": "failed to remove plugin installation status", - "success": "plugin installation status removed successfully" - }, - "writeMysqlPassword": { - "description": "write MySQL root password", - "paramFail": "MySQL root password is required", - "fail": "failed to write MySQL root password", - "success": "MySQL root password written successfully" - }, - "writeSite": { - "description": "write website data to the panel", - "paramFail": "name and path are required", - "siteExist": "website already exists", - "pathNotExist": "website directory does not exist", - "fail": "failed to write to website", - "success": "writing to website successfully" - }, - "deleteSite": { - "description": "delete panel website data", - "paramFail": "website name is required", - "fail": "failed to delete website", - "success": "website deleted successfully" - }, - "getSetting": { - "description": "get panel setting data", - "paramFail": "key is required" - }, - "writeSetting": { - "description": "write/update panel setting data", - "paramFail": "key and value are required", - "fail": "Writing settings failed", - "success": "settings written successfully" - }, - "deleteSetting": { - "description": "delete panel setting data", - "paramFail": "key is required", - "fail": "failed to delete settings", - "success": "settings deleted successfully" - } - }, - "panel:task": { - "description": "[Panel] Daily tasks" - } - }, - "errors": { - "internal": "internal system error", - "plugin": { - "notExist": "plugin does not exist", - "notInstalled": "plugin :slug is not installed", - "dependent": "plugin :slug requires dependency :dependency", - "incompatible": "plugin :slug is incompatible with :exclude plugin" - } - }, - "status": { - "upgrade": "Panel is currently undergoing an upgrade. Please try again later.", - "maintain": "Panel is currently undergoing maintenance. Please try again later.", - "closed": "Panel is closed.", - "failed": "Panel encountered an error during operation. Please check the troubleshooting or contact support." - } -} \ No newline at end of file diff --git a/lang/zh_CN.json b/lang/zh_CN.json deleted file mode 100644 index 3cbffd00..00000000 --- a/lang/zh_CN.json +++ /dev/null @@ -1,204 +0,0 @@ -{ - "auth": { - "session": { - "expired": "会话已过期", - "missing": "请启用 Cookie 后再试", - "invalid": "会话无效" - } - }, - "commands": { - "panel:cert-renew": { - "description": "[面板] 证书续签" - }, - "panel:monitoring": { - "description": "[面板] 系统监控", - "fail": "[面板] 系统监控保存失败" - }, - "panel": { - "description": "[面板] 命令行", - "forDeveloper": "以下命令请在开发者指导下使用", - "use": "请使用以下命令", - "tool": "命令行工具", - "port": "面板端口", - "portFail": "获取面板端口失败", - "entrance": "面板入口", - "update": { - "description": "更新 / 修复面板到最新版本", - "taskCheck": "当前有任务正在执行,禁止更新", - "dbFail": "面板数据库异常,已终止操作", - "versionFail": "获取最新版本失败", - "fail": "更新失败", - "success": "更新成功" - }, - "getInfo": { - "description": "重新初始化面板账号信息", - "adminGetFail": "获取管理员信息失败", - "adminSaveFail": "保存管理员信息失败", - "passwordGenerationFail": "生成密码失败", - "username": "用户名", - "password": "密码", - "address": "面板地址" - }, - "getPort": { - "description": "获取面板访问端口" - }, - "getEntrance": { - "description": "获取面板访问入口" - }, - "deleteEntrance": { - "description": "删除面板访问入口", - "fail": "删除面板入口失败", - "success": "删除面板入口成功" - }, - "cleanTask": { - "description": "清理面板运行中和等待中的任务[任务卡住时使用]", - "fail": "清理任务失败", - "success": "清理任务成功" - }, - "backup": { - "description": "备份网站 / MySQL数据库 / PostgreSQL数据库到指定目录并保留指定数量", - "paramFail": "参数错误", - "start": "开始备份", - "backupDirFail": "创建备份目录失败", - "targetSite": "目标网站", - "siteNotExist": "网站不存在", - "backupFail": "备份失败", - "backupSuccess": "备份成功", - "mysqlBackupFail": "备份MySQL数据库失败", - "targetMysql": "目标MySQL数据库", - "startExport": "开始导出", - "exportFail": "导出失败", - "exportSuccess": "导出成功", - "startCompress": "开始压缩", - "compressFail": "压缩失败", - "compressSuccess": "压缩成功", - "startMove": "开始移动", - "moveFail": "移动失败", - "moveSuccess": "移动成功", - "databaseGetFail": "获取数据库失败", - "databaseNotExist": "数据库不存在", - "targetPostgres": "目标PostgreSQL数据库", - "cleanBackup": "清理备份", - "cleanupFail": "清理失败", - "cleanupSuccess": "清理完成", - "deleteFail": "删除失败", - "success": "备份完成" - }, - "cutoff": { - "description": "切割网站日志并保留指定数量", - "paramFail": "参数错误", - "start": "开始切割", - "targetSite": "目标网站", - "siteNotExist": "网站不存在", - "logNotExist": "日志文件不存在", - "backupFail": "备份失败", - "clearFail": "清空失败", - "cleanupFail": "清理失败", - "clearLog": "清理日志", - "cutSuccess": "切割成功", - "cleanupSuccess": "清理完成", - "end": "切割完成" - }, - "installPlugin": { - "description": "安装插件", - "paramFail": "参数错误", - "success": "任务已提交" - }, - "uninstallPlugin": { - "description": "卸载插件", - "paramFail": "参数错误", - "success": "任务已提交" - }, - "updatePlugin": { - "description": "更新插件", - "paramFail": "参数错误", - "success": "任务已提交" - }, - "addSite": { - "description": "添加网站[域名和端口用英文逗号分隔]", - "paramFail": "参数错误", - "siteExist": "网站名已存在", - "success": "网站添加成功" - }, - "removeSite": { - "description": "删除网站", - "paramFail": "参数错误", - "siteNotExist": "网站名不存在", - "success": "网站删除成功" - }, - "init": { - "description": "初始化面板", - "exist": "面板已初始化", - "success": "初始化成功", - "adminFail": "创建管理员失败", - "fail": "初始化失败" - }, - "writePlugin": { - "description": "写入插件安装状态", - "paramFail": "参数错误", - "fail": "写入插件安装状态失败", - "success": "写入插件安装状态成功" - }, - "deletePlugin": { - "description": "移除插件安装状态", - "paramFail": "参数错误", - "fail": "移除插件安装状态失败", - "success": "移除插件安装状态成功" - }, - "writeMysqlPassword": { - "description": "写入MySQL root密码", - "paramFail": "参数错误", - "fail": "写入MySQL root密码失败", - "success": "写入MySQL root密码成功" - }, - "writeSite": { - "description": "写入网站数据到面板", - "paramFail": "参数错误", - "siteExist": "网站已存在", - "pathNotExist": "网站目录不存在", - "fail": "写入网站失败", - "success": "写入网站成功" - }, - "deleteSite": { - "description": "删除面板网站数据", - "paramFail": "参数错误", - "fail": "删除网站失败", - "success": "删除网站成功" - }, - "getSetting": { - "description": "获取面板设置数据", - "paramFail": "参数错误" - }, - "writeSetting": { - "description": "写入 / 更新面板设置数据", - "paramFail": "参数错误", - "fail": "写入设置失败", - "success": "写入设置成功" - }, - "deleteSetting": { - "description": "删除面板设置数据", - "paramFail": "参数错误", - "fail": "删除设置失败", - "success": "删除设置成功" - } - }, - "panel:task": { - "description": "[面板] 每日任务" - } - }, - "errors": { - "internal": "系统内部错误", - "plugin": { - "notExist": "插件不存在", - "notInstalled": "插件 :slug 未安装", - "dependent": "插件 :slug 需要依赖 :dependency 插件", - "incompatible": "插件 :slug 不兼容 :exclude 插件" - } - }, - "status": { - "upgrade": "面板升级中,请稍后", - "maintain": "面板正在运行维护,请稍后", - "closed": "面板已关闭", - "failed": "面板运行出错,请检查排除或联系支持" - } -} \ No newline at end of file diff --git a/main.go b/main.go deleted file mode 100644 index 4bf4c5a4..00000000 --- a/main.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ -package main - -import ( - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/bootstrap" -) - -// @title 耗子面板 API -// @version 2 -// @description 耗子面板的 API 信息 - -// @contact.name 耗子科技 -// @contact.email admin@haozi.net - -// @license.name GNU Affero General Public License v3 -// @license url https://www.gnu.org/licenses/agpl-3.0.html - -// @securityDefinitions.apikey BearerToken -// @in header -// @name Authorization - -// @BasePath /api -func main() { - // 启动框架 - bootstrap.Boot() - - // 启动 HTTP 服务 - if facades.Config().GetBool("panel.ssl") { - go func() { - if err := facades.Route().RunTLS(); err != nil { - facades.Log().Infof("Route run error: %v", err) - } - }() - } else { - go func() { - if err := facades.Route().Run(); err != nil { - facades.Log().Infof("Route run error: %v", err) - } - }() - } - - // 启动计划任务 - go facades.Schedule().Run() - - // 启动队列 - go func() { - if err := facades.Queue().Worker(nil).Run(); err != nil { - facades.Log().Errorf("Queue run error: %v", err) - } - }() - - select {} -} diff --git a/panel-example.conf b/panel-example.conf deleted file mode 100644 index 3e440557..00000000 --- a/panel-example.conf +++ /dev/null @@ -1,10 +0,0 @@ -APP_ENV=local -APP_KEY= -APP_DEBUG=false -APP_PORT=8888 -APP_ENTRANCE=/ -APP_SSL=false -APP_LOCALE=zh_CN - -JWT_SECRET= -SESSION_LIFETIME=120 diff --git a/pkg/acme/acme.go b/pkg/acme/acme.go index 368d40c6..46be6d2c 100644 --- a/pkg/acme/acme.go +++ b/pkg/acme/acme.go @@ -14,7 +14,7 @@ import ( "github.com/mholt/acmez/v2/acme" "go.uber.org/zap" - "github.com/TheTNB/panel/v2/pkg/cert" + "github.com/TheTNB/panel/pkg/cert" ) const ( diff --git a/pkg/acme/client.go b/pkg/acme/client.go index 7826a343..d71f8046 100644 --- a/pkg/acme/client.go +++ b/pkg/acme/client.go @@ -8,7 +8,7 @@ import ( "github.com/mholt/acmez/v2" "github.com/mholt/acmez/v2/acme" - "github.com/TheTNB/panel/v2/pkg/cert" + "github.com/TheTNB/panel/pkg/cert" ) type Certificate struct { diff --git a/pkg/acme/solvers.go b/pkg/acme/solvers.go index a70fe44f..8c962a8c 100644 --- a/pkg/acme/solvers.go +++ b/pkg/acme/solvers.go @@ -15,8 +15,8 @@ import ( "github.com/mholt/acmez/v2/acme" "golang.org/x/net/publicsuffix" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" ) type httpSolver struct { diff --git a/pkg/arch/arch.go b/pkg/arch/arch.go new file mode 100644 index 00000000..92b98f25 --- /dev/null +++ b/pkg/arch/arch.go @@ -0,0 +1,21 @@ +package arch + +import "runtime" + +// IsArm returns whether the current CPU architecture is ARM. +// IsArm 返回当前 CPU 架构是否为 ARM。 +func IsArm() bool { + return runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" +} + +// IsX86 returns whether the current CPU architecture is X86. +// IsX86 返回当前 CPU 架构是否为 X86。 +func IsX86() bool { + return runtime.GOARCH == "386" || runtime.GOARCH == "amd64" +} + +// Is64Bit returns whether the current CPU architecture is 64-bit. +// Is64Bit 返回当前 CPU 架构是否为 64 位。 +func Is64Bit() bool { + return runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" +} diff --git a/pkg/db/mysql.go b/pkg/db/mysql.go index db03f200..62f56b31 100644 --- a/pkg/db/mysql.go +++ b/pkg/db/mysql.go @@ -6,7 +6,7 @@ import ( _ "github.com/go-sql-driver/mysql" - "github.com/TheTNB/panel/v2/pkg/types" + "github.com/TheTNB/panel/pkg/types" ) type MySQL struct { diff --git a/pkg/db/mysql_tools.go b/pkg/db/mysql_tools.go index 21231c8b..cb8201cb 100644 --- a/pkg/db/mysql_tools.go +++ b/pkg/db/mysql_tools.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" ) // MySQLResetRootPassword 重置 MySQL root密码 diff --git a/pkg/db/postgres.go b/pkg/db/postgres.go index 30953915..bf2a4915 100644 --- a/pkg/db/postgres.go +++ b/pkg/db/postgres.go @@ -6,10 +6,10 @@ import ( _ "github.com/lib/pq" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" - "github.com/TheTNB/panel/v2/pkg/systemctl" - "github.com/TheTNB/panel/v2/pkg/types" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" + "github.com/TheTNB/panel/pkg/types" ) type Postgres struct { @@ -90,7 +90,7 @@ func (m *Postgres) UserDrop(user string) error { return err } - _, _ = shell.Execf(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`) + _, _ = shell.Execf(`sed -i '/%s/d' /www/server/postgresql/data/pg_hba.conf`, user) return systemctl.Reload("postgresql") } @@ -126,7 +126,7 @@ func (m *Postgres) HostAdd(database, user, host string) error { func (m *Postgres) HostRemove(database, user, host string) error { regex := fmt.Sprintf(`host\s+%s\s+%s\s+%s`, database, user, host) - if _, err := shell.Execf(`sed -i '/` + regex + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil { + if _, err := shell.Execf(`sed -i '/%s/d' /www/server/postgresql/data/pg_hba.conf`, regex); err != nil { return err } diff --git a/pkg/firewall/consts.go b/pkg/firewall/consts.go new file mode 100644 index 00000000..87c259c4 --- /dev/null +++ b/pkg/firewall/consts.go @@ -0,0 +1,24 @@ +package firewall + +type FireInfo struct { + Family string `json:"family"` // ipv4 ipv6 + Address string `json:"address"` + Port uint `json:"port"` // 1-65535 + Protocol string `json:"protocol"` // tcp udp tcp/udp + Strategy string `json:"strategy"` // accept drop + + Num string `json:"num"` + TargetIP string `json:"targetIP"` + TargetPort string `json:"targetPort"` // 1-65535 + + UsedStatus string `json:"usedStatus"` + Description string `json:"description"` +} + +type Forward struct { + Num string `json:"num"` + Protocol string `json:"protocol"` + Port uint `json:"port"` // 1-65535 + TargetIP string `json:"targetIP"` + TargetPort uint `json:"targetPort"` // 1-65535 +} diff --git a/pkg/firewall/firewall.go b/pkg/firewall/firewall.go new file mode 100644 index 00000000..9fb05caa --- /dev/null +++ b/pkg/firewall/firewall.go @@ -0,0 +1,218 @@ +package firewall + +import ( + "errors" + "fmt" + "regexp" + "strings" + "sync" + + "github.com/spf13/cast" + + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/systemctl" +) + +type Firewall struct { + forwardListRegex *regexp.Regexp + richRuleRegex *regexp.Regexp +} + +func NewFirewall() *Firewall { + firewall := &Firewall{ + forwardListRegex: regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`), + richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)" (?:source address="([^"]+)" )?(?:port port="([^"]+)" )?(?:protocol="([^"]+)" )?(accept|drop|reject)$`), + } + + return firewall +} + +func (r *Firewall) Status() (bool, error) { + return systemctl.Status("firewalld") +} + +func (r *Firewall) Version() (string, error) { + return shell.Execf("firewall-cmd --version") +} + +func (r *Firewall) ListRule() ([]FireInfo, error) { + var wg sync.WaitGroup + var data []FireInfo + wg.Add(2) + + go func() { + defer wg.Done() + out, err := shell.Execf("firewall-cmd --zone=public --list-ports") + if err != nil { + return + } + ports := strings.Split(out, " ") + for _, port := range ports { + if len(port) == 0 { + continue + } + var itemPort FireInfo + if strings.Contains(port, "/") { + itemPort.Port = cast.ToUint(strings.Split(port, "/")[0]) + itemPort.Protocol = strings.Split(port, "/")[1] + } + itemPort.Strategy = "accept" + data = append(data, itemPort) + } + }() + go func() { + defer wg.Done() + rich, err := r.ListRichRule() + if err != nil { + return + } + + data = append(data, rich...) + }() + + wg.Wait() + return data, nil +} + +func (r *Firewall) ListForward() ([]FireInfo, error) { + out, err := shell.Execf("firewall-cmd --zone=public --list-forward-ports") + if err != nil { + return nil, err + } + + var data []FireInfo + for _, line := range strings.Split(out, "\n") { + line = strings.TrimFunc(line, func(r rune) bool { + return r <= 32 + }) + if r.forwardListRegex.MatchString(line) { + match := r.forwardListRegex.FindStringSubmatch(line) + if len(match) < 4 { + continue + } + if len(match[4]) == 0 { + match[4] = "127.0.0.1" + } + data = append(data, FireInfo{ + Port: cast.ToUint(match[1]), + Protocol: match[2], + TargetIP: match[4], + TargetPort: match[3], + }) + } + } + + return data, nil +} + +func (r *Firewall) ListRichRule() ([]FireInfo, error) { + out, err := shell.Execf("firewall-cmd --zone=public --list-rich-rules") + if err != nil { + return nil, err + } + + var data []FireInfo + rules := strings.Split(out, "\n") + for _, rule := range rules { + if len(rule) == 0 { + continue + } + if itemRule, err := r.parseRichRule(rule); err == nil { + data = append(data, *itemRule) + } + } + + return data, nil +} + +func (r *Firewall) Port(port FireInfo, operation string) error { + stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d/%s --permanent", operation, port.Port, port.Protocol) + if err != nil { + return fmt.Errorf("%s port %d/%s failed, err: %s", operation, port.Port, port.Protocol, stdout) + } + return systemctl.Reload("firewalld") +} + +func (r *Firewall) RichRules(rule FireInfo, operation string) error { + families := strings.Split(rule.Family, "/") // ipv4 ipv6 + + for _, family := range families { + var ruleStr strings.Builder + ruleStr.WriteString(fmt.Sprintf(`rule family="%s" `, family)) + if len(rule.Address) != 0 { + ruleStr.WriteString(fmt.Sprintf(`source address="%s" `, rule.Address)) + } + if rule.Port != 0 { + ruleStr.WriteString(fmt.Sprintf(`port port="%d" `, rule.Port)) + } + if len(rule.Protocol) != 0 { + ruleStr.WriteString(fmt.Sprintf(`protocol="%s" `, rule.Protocol)) + } + + ruleStr.WriteString(rule.Strategy) + out, err := shell.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleStr.String()) + if err != nil { + return fmt.Errorf("%s rich rules (%s) failed, err: %s", operation, ruleStr.String(), out) + } + } + + return systemctl.Reload("firewalld") +} + +func (r *Firewall) PortForward(info Forward, operation string) error { + if err := r.enableForward(); err != nil { + return err + } + + var ruleStr strings.Builder + ruleStr.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, info.Port, info.Protocol)) + if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" { + ruleStr.WriteString(fmt.Sprintf("toaddr=%s:toport=%d", info.TargetIP, info.TargetPort)) + } else { + ruleStr.WriteString(fmt.Sprintf("toport=%d", info.TargetPort)) + } + ruleStr.WriteString(" --permanent") + + out, err := shell.Execf(ruleStr.String()) // nolint: govet + if err != nil { + return fmt.Errorf("%s port forward failed, err: %s", operation, out) + } + + return systemctl.Reload("firewalld") +} + +func (r *Firewall) parseRichRule(line string) (*FireInfo, error) { + itemRule := new(FireInfo) + if r.richRuleRegex.MatchString(line) { + match := r.richRuleRegex.FindStringSubmatch(line) + if len(match) < 6 { + return nil, errors.New("invalid rich rule") + } + + itemRule.Family = match[1] + itemRule.Address = match[2] + itemRule.Port = cast.ToUint(match[3]) + itemRule.Protocol = match[4] + itemRule.Strategy = match[5] + } + + return itemRule, nil +} + +func (r *Firewall) enableForward() error { + out, err := shell.Execf("firewall-cmd --zone=public --query-masquerade") + if err != nil { + if out == "no" { + out, err = shell.Execf("firewall-cmd --zone=public --add-masquerade --permanent") + if err != nil { + return fmt.Errorf("%s: %s", err, out) + } + + return systemctl.Reload("firewalld") + } + + return fmt.Errorf("%s: %s", err, out) + } + + return nil +} diff --git a/pkg/h/request.go b/pkg/h/request.go deleted file mode 100644 index b321d6bc..00000000 --- a/pkg/h/request.go +++ /dev/null @@ -1,29 +0,0 @@ -package h - -import "github.com/goravel/framework/contracts/http" - -// SanitizeRequest 消毒请求参数 -func SanitizeRequest(ctx http.Context, request http.FormRequest) http.Response { - errors, err := ctx.Request().ValidateRequest(request) - if err != nil { - return Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - if errors != nil { - return Error(ctx, http.StatusUnprocessableEntity, errors.One()) - } - - return nil -} - -// Sanitize 消毒参数 -func Sanitize(ctx http.Context, rules map[string]string) http.Response { - validator, err := ctx.Request().Validate(rules) - if err != nil { - return Error(ctx, http.StatusUnprocessableEntity, err.Error()) - } - if validator.Fails() { - return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One()) - } - - return nil -} diff --git a/pkg/h/response.go b/pkg/h/response.go deleted file mode 100644 index 5a2c8b90..00000000 --- a/pkg/h/response.go +++ /dev/null @@ -1,68 +0,0 @@ -package h - -import ( - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/http/requests/common" -) - -// SuccessResponse 通用成功响应 -type SuccessResponse struct { - Message string `json:"message"` - Data any `json:"data"` -} - -// ErrorResponse 通用错误响应 -type ErrorResponse struct { - Message string `json:"message"` -} - -// Success 响应成功 -func Success(ctx http.Context, data any) http.Response { - return ctx.Response().Success().Json(&SuccessResponse{ - Message: "success", - Data: data, - }) -} - -// Error 响应错误 -func Error(ctx http.Context, code int, message string) http.Response { - return ctx.Response().Json(code, &ErrorResponse{ - Message: message, - }) -} - -// ErrorSystem 响应系统错误 -func ErrorSystem(ctx http.Context) http.Response { - return ctx.Response().Json(http.StatusInternalServerError, &ErrorResponse{ - Message: facades.Lang(ctx).Get("errors.internal"), - }) -} - -// Paginate 取分页条目 -func Paginate[T any](ctx http.Context, allItems []T) (pagedItems []T, total int) { - var paginateRequest commonrequests.Paginate - sanitize := SanitizeRequest(ctx, &paginateRequest) - if sanitize != nil { - return []T{}, 0 - } - - page := ctx.Request().QueryInt("page", 1) - limit := ctx.Request().QueryInt("limit", 10) - total = len(allItems) - startIndex := (page - 1) * limit - endIndex := page * limit - - if total == 0 { - return []T{}, 0 - } - if startIndex > total { - return []T{}, total - } - if endIndex > total { - endIndex = total - } - - return allItems[startIndex:endIndex], total -} diff --git a/pkg/io/file.go b/pkg/io/file.go index 88495db5..4cde111a 100644 --- a/pkg/io/file.go +++ b/pkg/io/file.go @@ -1,11 +1,33 @@ package io import ( + "archive/zip" + "context" + "errors" + stdio "io" "os" + "path" "path/filepath" "strings" - "github.com/mholt/archiver/v3" + "github.com/mholt/archiver/v4" +) + +type FormatArchive string + +const ( + Zip FormatArchive = "zip" + Gz FormatArchive = "gz" + Bz2 FormatArchive = "bz2" + Tar FormatArchive = "tar" + TarGz FormatArchive = "tar.gz" + Xz FormatArchive = "xz" + SevenZip FormatArchive = "7z" +) + +var ( + ErrFormatNotSupport = errors.New("不支持此格式") + ErrNotDirectory = errors.New("目标不是目录") ) // Write 写入文件 @@ -49,14 +71,102 @@ func FileInfo(path string) (os.FileInfo, error) { return os.Stat(path) } -// UnArchive 智能解压文件 -func UnArchive(file string, dst string) error { - return archiver.Unarchive(file, dst) +// Compress 压缩文件 +func Compress(src []string, dst string, format FormatArchive) error { + // 不支持7z + if format == SevenZip { + return ErrFormatNotSupport + } + arch := getFormat(format) + + srcMap := make(map[string]string, len(src)) + for _, s := range src { + base := filepath.Base(s) + srcMap[s] = base + } + + dir := filepath.Dir(dst) + if !Exists(dir) { + if err := Mkdir(dir, 0755); err != nil { + return err + } + } + + files, err := archiver.FilesFromDisk(nil, srcMap) + if err != nil { + return err + } + + out, err := os.OpenFile(dst, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer out.Close() + + err = arch.Archive(context.Background(), out, files) + if err != nil { + _ = Remove(dst) + } + + return nil } -// Archive 智能压缩文件 -func Archive(src []string, dst string) error { - return archiver.Archive(src, dst) +// UnCompress 解压文件 +func UnCompress(src string, dst string, format FormatArchive) error { + handler := func(ctx context.Context, f archiver.File) error { + info := f.FileInfo + fileName := f.NameInArchive + filePath := filepath.Join(dst, fileName) + + if f.FileInfo.IsDir() { + if err := Mkdir(filePath, info.Mode()); err != nil { + return err + } + return nil + } + + parentDir := path.Dir(filePath) + if !Exists(parentDir) { + if err := Mkdir(parentDir, info.Mode()); err != nil { + return err + } + } + + r, err := f.Open() + if err != nil { + return err + } + w, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer r.Close() + defer w.Close() + + if _, err = stdio.Copy(w, r); err != nil { + return err + } + + return nil + } + + arch := getFormat(format) + file, err := os.Open(src) + if err != nil { + return err + } + defer file.Close() + + if !Exists(dst) { + if err = Mkdir(dst, 0755); err != nil { + return err + } + } + if !IsDir(dst) { + return ErrNotDirectory + } + + return arch.Extract(context.Background(), file, nil, handler) } // TempFile 创建临时文件 @@ -83,3 +193,28 @@ func GetSymlink(path string) string { } return linkPath } + +func getFormat(f FormatArchive) archiver.CompressedArchive { + format := archiver.CompressedArchive{} + switch f { + case Tar: + format.Archival = archiver.Tar{} + case TarGz, Gz: + format.Compression = archiver.Gz{} + format.Archival = archiver.Tar{} + case Zip: + format.Archival = archiver.Zip{ + Compression: zip.Deflate, + } + case Bz2: + format.Compression = archiver.Bz2{} + format.Archival = archiver.Tar{} + case Xz: + format.Compression = archiver.Xz{} + format.Archival = archiver.Tar{} + case SevenZip: + format.Archival = archiver.SevenZip{} + + } + return format +} diff --git a/pkg/io/io_test.go b/pkg/io/io_test.go new file mode 100644 index 00000000..ec885b81 --- /dev/null +++ b/pkg/io/io_test.go @@ -0,0 +1,208 @@ +package io + +import ( + "os" + "path/filepath" + "testing" + + "github.com/go-rat/utils/env" + "github.com/stretchr/testify/suite" +) + +type IOTestSuite struct { + suite.Suite +} + +func TestIOTestSuite(t *testing.T) { + suite.Run(t, &IOTestSuite{}) +} + +func (s *IOTestSuite) SetupTest() { + if _, err := os.Stat("testdata"); os.IsNotExist(err) { + s.NoError(os.MkdirAll("testdata", 0755)) + } +} + +func (s *IOTestSuite) TearDownTest() { + s.NoError(os.RemoveAll("testdata")) +} + +func (s *IOTestSuite) TestWriteCreatesFileWithCorrectContent() { + path := "testdata/write_test.txt" + data := "Hello, World!" + permission := os.FileMode(0644) + + s.NoError(Write(path, data, permission)) + + content, err := Read(path) + s.NoError(err) + s.Equal(data, content) +} + +func (s *IOTestSuite) TestWriteAppendAppendsToFile() { + path := "testdata/append_test.txt" + initialData := "Hello" + appendData := ", World!" + + s.NoError(Write(path, initialData, 0644)) + s.NoError(WriteAppend(path, appendData)) + + content, err := Read(path) + s.NoError(err) + s.Equal("Hello, World!", content) +} + +func (s *IOTestSuite) TestCompress() { + src := []string{"testdata/compress_test1.txt", "testdata/compress_test2.txt"} + err := Write(src[0], "File 1", 0644) + s.NoError(err) + err = Write(src[1], "File 2", 0644) + s.NoError(err) + + err = Compress(src, "testdata/compress_test.zip", Zip) + s.NoError(err) +} + +func (s *IOTestSuite) TestUnCompress() { + src := []string{"testdata/uncompress_test1.txt", "testdata/uncompress_test2.txt"} + err := Write(src[0], "File 1", 0644) + s.NoError(err) + err = Write(src[1], "File 2", 0644) + s.NoError(err) + + err = Compress(src, "testdata/uncompress_test.zip", Zip) + s.NoError(err) + + err = UnCompress("testdata/uncompress_test.zip", "testdata/uncompressed", Zip) + s.NoError(err) + + data, err := Read("testdata/uncompressed/uncompress_test1.txt") + s.NoError(err) + s.Equal("File 1", data) + + data, err = Read("testdata/uncompressed/uncompress_test2.txt") + s.NoError(err) + s.Equal("File 2", data) +} + +func (s *IOTestSuite) TestRemoveDeletesFileOrDirectory() { + path := "testdata/remove_test" + s.NoError(Mkdir(path, 0755)) + s.DirExists(path) + + s.NoError(Remove(path)) + s.NoDirExists(path) +} + +func (s *IOTestSuite) TestMkdirCreatesDirectory() { + path := "testdata/mkdir_test" + s.NoError(Mkdir(path, 0755)) + s.DirExists(path) +} + +func (s *IOTestSuite) TestChmodChangesPermissions() { + if env.IsWindows() { + s.T().Skip("Skipping on Windows") + } + path := "testdata/chmod_test.txt" + s.NoError(Write(path, "test", 0644)) + + s.NoError(Chmod(path, 0755)) + info, err := os.Stat(path) + s.NoError(err) + s.Equal(os.FileMode(0755), info.Mode().Perm()) +} + +func (s *IOTestSuite) TestChownChangesOwner() { + if env.IsWindows() { + s.T().Skip("Skipping on Windows") + } + path := "testdata/chown_test.txt" + s.NoError(Write(path, "test", 0644)) + + s.NoError(Chown(path, "root", "root")) +} + +func (s *IOTestSuite) TestExistsReturnsTrueForExistingPath() { + path := "testdata/exists_test.txt" + s.NoError(Write(path, "test", 0644)) + s.True(Exists(path)) +} + +func (s *IOTestSuite) TestExistsReturnsFalseForNonExistingPath() { + path := "testdata/nonexistent.txt" + s.False(Exists(path)) +} + +func (s *IOTestSuite) TestEmptyReturnsTrueForEmptyDirectory() { + path := "testdata/empty_test" + s.NoError(Mkdir(path, 0755)) + s.True(Empty(path)) +} + +func (s *IOTestSuite) TestEmptyReturnsFalseForNonEmptyDirectory() { + path := "testdata/nonempty_test" + s.NoError(Mkdir(path, 0755)) + s.NoError(Write(filepath.Join(path, "file.txt"), "test", 0644)) + s.False(Empty(path)) +} + +func (s *IOTestSuite) TestMvMovesFile() { + src := "testdata/mv_src.txt" + dst := "testdata/mv_dst.txt" + s.NoError(Write(src, "test", 0644)) + + s.NoError(Mv(src, dst)) + s.FileExists(dst) + s.NoFileExists(src) +} + +func (s *IOTestSuite) TestCpCopiesFile() { + src := "testdata/cp_src.txt" + dst := "testdata/cp_dst.txt" + s.NoError(Write(src, "test", 0644)) + + s.NoError(Cp(src, dst)) + s.FileExists(dst) + s.FileExists(src) +} + +func (s *IOTestSuite) TestSizeReturnsCorrectSize() { + path := "testdata/size_test.txt" + data := "12345" + s.NoError(Write(path, data, 0644)) + + size, err := Size(path) + s.NoError(err) + s.Equal(int64(len(data)), size) +} + +func (s *IOTestSuite) TestTempDirCreatesTemporaryDirectory() { + dir, err := TempDir("tempdir_test") + s.NoError(err) + s.DirExists(dir) + s.NoError(Remove(dir)) +} + +func (s *IOTestSuite) TestReadDirReturnsDirectoryEntries() { + path := "testdata/readdir_test" + s.NoError(Mkdir(path, 0755)) + s.NoError(Write(filepath.Join(path, "file1.txt"), "test", 0644)) + s.NoError(Write(filepath.Join(path, "file2.txt"), "test", 0644)) + + entries, err := ReadDir(path) + s.NoError(err) + s.Len(entries, 2) +} + +func (s *IOTestSuite) TestIsDirReturnsTrueForDirectory() { + path := "testdata/isdir_test" + s.NoError(Mkdir(path, 0755)) + s.True(IsDir(path)) +} + +func (s *IOTestSuite) TestIsDirReturnsFalseForFile() { + path := "testdata/isfile_test.txt" + s.NoError(Write(path, "test", 0644)) + s.False(IsDir(path)) +} diff --git a/pkg/io/path.go b/pkg/io/path.go index e20eab8a..9dbe6491 100644 --- a/pkg/io/path.go +++ b/pkg/io/path.go @@ -20,13 +20,13 @@ func Mkdir(path string, permission os.FileMode) error { // Chmod 修改文件/目录权限 func Chmod(path string, permission os.FileMode) error { - cmd := exec.Command("chmod", "-R", fmt.Sprintf("%o", permission), path) + cmd := exec.Command("sudo", "chmod", "-R", fmt.Sprintf("%o", permission), path) return cmd.Run() } // Chown 修改文件或目录所有者 func Chown(path, user, group string) error { - cmd := exec.Command("chown", "-R", user+":"+group, path) + cmd := exec.Command("sudo", "chown", "-R", user+":"+group, path) return cmd.Run() } @@ -149,3 +149,12 @@ func TempDir(prefix string) (string, error) { func ReadDir(path string) ([]os.DirEntry, error) { return os.ReadDir(path) } + +// IsDir 判断是否为目录 +func IsDir(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + return info.IsDir() +} diff --git a/pkg/migrate/migrate.go b/pkg/migrate/migrate.go deleted file mode 100644 index 0f186055..00000000 --- a/pkg/migrate/migrate.go +++ /dev/null @@ -1,22 +0,0 @@ -package migrate - -import ( - "fmt" - - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" -) - -func Migrate(db *gorm.DB) { - options := &gormigrate.Options{ - TableName: "new_migrations", - IDColumnName: "id", - IDColumnSize: 255, - } - migrator := gormigrate.New(db, options, []*gormigrate.Migration{ - Init, - }) - if err := migrator.Migrate(); err != nil { - panic(fmt.Sprintf("Failed to migrate database: %v", err)) - } -} diff --git a/pkg/migrate/migrations.go b/pkg/migrate/migrations.go deleted file mode 100644 index ae988824..00000000 --- a/pkg/migrate/migrations.go +++ /dev/null @@ -1,42 +0,0 @@ -package migrate - -import ( - "github.com/go-gormigrate/gormigrate/v2" - "gorm.io/gorm" - - "github.com/TheTNB/panel/v2/app/models" -) - -var Init = &gormigrate.Migration{ - ID: "20240624-init", - Migrate: func(tx *gorm.DB) error { - return tx.AutoMigrate( - &models.Cert{}, - &models.CertDNS{}, - &models.CertUser{}, - &models.Cron{}, - &models.Database{}, - &models.Monitor{}, - &models.Plugin{}, - &models.Setting{}, - &models.Task{}, - &models.User{}, - &models.Website{}, - ) - }, - Rollback: func(tx *gorm.DB) error { - return tx.Migrator().DropTable( - &models.Cert{}, - &models.CertDNS{}, - &models.CertUser{}, - &models.Cron{}, - &models.Database{}, - &models.Monitor{}, - &models.Plugin{}, - &models.Setting{}, - &models.Task{}, - &models.User{}, - &models.Website{}, - ) - }, -} diff --git a/pkg/ntp/ntp.go b/pkg/ntp/ntp.go new file mode 100644 index 00000000..21b34601 --- /dev/null +++ b/pkg/ntp/ntp.go @@ -0,0 +1,119 @@ +package ntp + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/beevik/ntp" + + "github.com/TheTNB/panel/pkg/shell" +) + +var ErrNotReachable = errors.New("无法连接到 NTP 服务器") + +var ErrNoAvailableServer = errors.New("无可用的 NTP 服务器") + +var defaultAddresses = []string{ + //"ntp.ntsc.ac.cn", // 中科院国家授时中心的服务器很快,但是多刷几次就会被封 + "ntp.aliyun.com", // 阿里云 + "ntp1.aliyun.com", // 阿里云2 + "ntp.tencent.com", // 腾讯云 + "time.windows.com", // Windows + "time.apple.com", // Apple +} + +func Now(address ...string) (time.Time, error) { + if len(address) > 0 { + if now, err := ntp.Time(address[0]); err != nil { + return time.Now(), fmt.Errorf("%w: %s", ErrNotReachable, err) + } else { + return now, nil + } + } + + best, err := bestServer(defaultAddresses...) + if err != nil { + return time.Now(), err + } + + now, err := ntp.Time(best) + if err != nil { + return time.Now(), fmt.Errorf("%w: %s", ErrNotReachable, err) + } + + return now, nil +} + +func UpdateSystemTime(time time.Time) error { + _, err := shell.Execf(`sudo date -s "%s"`, time.Format("2006-01-02 15:04:05")) + return err +} + +func UpdateSystemTimeZone(timezone string) error { + _, err := shell.Execf(`sudo timedatectl set-timezone %s`, timezone) + return err +} + +// pingServer 计算NTP服务器的延迟 +func pingServer(addr string) (time.Duration, error) { + options := ntp.QueryOptions{Timeout: 1 * time.Second} + response, err := ntp.QueryWithOptions(addr, options) + if err != nil { + return 0, err + } + + return response.RTT, nil +} + +// bestServer 返回延迟最低的NTP服务器 +func bestServer(addresses ...string) (string, error) { + if len(addresses) == 0 { + addresses = defaultAddresses + } + + type ntpResult struct { + address string + delay time.Duration + err error + } + + results := make(chan ntpResult, len(addresses)) + var wg sync.WaitGroup + + for _, addr := range addresses { + wg.Add(1) + go func(addr string) { + defer wg.Done() + + delay, err := pingServer(addr) + results <- ntpResult{address: addr, delay: delay, err: err} + }(addr) + } + + wg.Wait() + close(results) + + var bestAddr string + var bestDelay time.Duration + found := false + + for result := range results { + if result.err != nil { + continue + } + + if !found || result.delay < bestDelay { + bestAddr = result.address + bestDelay = result.delay + found = true + } + } + + if !found { + return "", ErrNoAvailableServer + } + + return bestAddr, nil +} diff --git a/pkg/ntp/ntp_test.go b/pkg/ntp/ntp_test.go new file mode 100644 index 00000000..6cee6a51 --- /dev/null +++ b/pkg/ntp/ntp_test.go @@ -0,0 +1,49 @@ +package ntp + +import ( + "testing" + "time" + + "github.com/go-rat/utils/env" + "github.com/stretchr/testify/suite" +) + +type NTPTestSuite struct { + suite.Suite +} + +func TestNTPTestSuite(t *testing.T) { + suite.Run(t, &NTPTestSuite{}) +} + +func (suite *NTPTestSuite) TestNowWithDefaultAddresses() { + now, _ := Now() + suite.WithinDuration(time.Now(), now, time.Minute) +} + +func (suite *NTPTestSuite) TestNowWithCustomAddress() { + now, err := Now("time.windows.com") + suite.NoError(err) + suite.WithinDuration(time.Now(), now, time.Minute) +} + +func (suite *NTPTestSuite) TestNowWithInvalidAddress() { + _, err := Now("invalid.address") + suite.Error(err) +} + +func (suite *NTPTestSuite) TestUpdateSystemTime() { + if env.IsWindows() { + suite.T().Skip("Skipping on Windows") + } + err := UpdateSystemTime(time.Now()) + suite.NoError(err) +} + +func (suite *NTPTestSuite) TestUpdateSystemTimeZone() { + if env.IsWindows() { + suite.T().Skip("Skipping on Windows") + } + err := UpdateSystemTimeZone("UTC") + suite.NoError(err) +} diff --git a/pkg/pluginloader/plugin.go b/pkg/pluginloader/plugin.go new file mode 100644 index 00000000..fa92b64a --- /dev/null +++ b/pkg/pluginloader/plugin.go @@ -0,0 +1,44 @@ +// Package pluginloader 面板插件加载器 +package pluginloader + +import ( + "fmt" + "sync" + + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/types" +) + +var plugins sync.Map + +func Register(plugin *types.Plugin) { + plugins.Store(plugin.Slug, plugin) +} + +func Get(slug string) (*types.Plugin, error) { + if plugin, ok := plugins.Load(slug); ok { + return plugin.(*types.Plugin), nil + } + return nil, fmt.Errorf("plugin %s not found", slug) +} + +func All() []*types.Plugin { + var list []*types.Plugin + plugins.Range(func(_, plugin any) bool { + if p, ok := plugin.(*types.Plugin); ok { + list = append(list, p) + } + return true + }) + return list +} + +func Boot(r chi.Router) { + plugins.Range(func(_, plugin any) bool { + if p, ok := plugin.(*types.Plugin); ok { + r.Route(fmt.Sprintf("/api/plugins/%s", p.Slug), p.Route) + } + return true + }) +} diff --git a/pkg/queue/job.go b/pkg/queue/job.go new file mode 100644 index 00000000..20432a31 --- /dev/null +++ b/pkg/queue/job.go @@ -0,0 +1,16 @@ +package queue + +type Job interface { + Handle(args ...any) error +} + +type JobWithErrHandle interface { + Job + ErrHandle(err error) +} + +type Jobs struct { + Job Job + Args []any + Delay uint +} diff --git a/pkg/queue/queue.go b/pkg/queue/queue.go new file mode 100644 index 00000000..f64cd049 --- /dev/null +++ b/pkg/queue/queue.go @@ -0,0 +1,92 @@ +package queue + +import ( + "errors" + "time" +) + +type Queue struct { + jobs chan Jobs + isShutdown chan struct{} + done chan struct{} +} + +func New() *Queue { + return &Queue{ + jobs: make(chan Jobs, 10), + isShutdown: make(chan struct{}), + done: make(chan struct{}), + } +} + +func (r *Queue) Push(job Job, args []any) error { + select { + case <-r.isShutdown: + return errors.New("queue is shutdown, cannot add new jobs") + default: + r.jobs <- Jobs{Job: job, Args: args} + return nil + } +} + +func (r *Queue) Bulk(jobs []Jobs) error { + for _, job := range jobs { + if job.Delay > 0 { + time.AfterFunc(time.Duration(job.Delay)*time.Second, func() { + select { + case <-r.isShutdown: + return + default: + r.jobs <- Jobs{Job: job.Job, Args: job.Args} + } + }) + continue + } + + select { + case <-r.isShutdown: + return errors.New("queue is shutdown, cannot add new jobs") + default: + r.jobs <- job + } + } + + return nil +} + +func (r *Queue) Later(delay uint, job Job, args []any) error { + time.AfterFunc(time.Duration(delay)*time.Second, func() { + select { + case <-r.isShutdown: + return + default: + r.jobs <- Jobs{Job: job, Args: args} + } + }) + + return nil +} + +func (r *Queue) Run() { + go func() { + for { + select { + case job := <-r.jobs: + if err := job.Job.Handle(job.Args...); err != nil { + if errJob, ok := job.Job.(JobWithErrHandle); ok { + errJob.ErrHandle(err) + } + } + case <-r.isShutdown: + close(r.done) + return + } + } + }() +} + +func (r *Queue) Shutdown() error { + close(r.isShutdown) + <-r.done + return nil +} diff --git a/pkg/queue/queue_test.go b/pkg/queue/queue_test.go new file mode 100644 index 00000000..db86a0fe --- /dev/null +++ b/pkg/queue/queue_test.go @@ -0,0 +1,140 @@ +package queue + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type QueueTestSuite struct { + suite.Suite +} + +func TestQueueTestSuite(t *testing.T) { + suite.Run(t, &QueueTestSuite{}) +} + +func (suite *QueueTestSuite) TestQueueInitialization() { + queue := New() + suite.NotNil(queue) + suite.NotNil(queue.jobs) + suite.NotNil(queue.isShutdown) + suite.NotNil(queue.done) +} + +func (suite *QueueTestSuite) TestPushJobToQueue() { + queue := New() + job := &MockJob{} + err := queue.Push(job, []any{"arg1", "arg2"}) + suite.NoError(err) +} + +func (suite *QueueTestSuite) TestPushJobToShutdownQueue() { + queue := New() + queue.Run() + suite.NoError(queue.Shutdown()) + job := &MockJob{} + err := queue.Push(job, []any{"arg1", "arg2"}) + suite.Error(err) + suite.EqualError(err, "queue is shutdown, cannot add new jobs") +} + +func (suite *QueueTestSuite) TestBulkJobsToQueue() { + queue := New() + jobs := []Jobs{ + {Job: &MockJob{}, Args: []any{"arg1"}}, + {Job: &MockJob{}, Args: []any{"arg2"}}, + } + err := queue.Bulk(jobs) + suite.NoError(err) +} + +func (suite *QueueTestSuite) TestBulkJobsToShutdownQueue() { + queue := New() + queue.Run() + suite.NoError(queue.Shutdown()) + jobs := []Jobs{ + {Job: &MockJob{}, Args: []any{"arg1"}}, + {Job: &MockJob{}, Args: []any{"arg2"}}, + } + err := queue.Bulk(jobs) + suite.Error(err) + suite.EqualError(err, "queue is shutdown, cannot add new jobs") +} + +func (suite *QueueTestSuite) TestLaterJobExecution() { + queue := New() + job := &MockJob{} + err := queue.Later(1, job, []any{"arg1"}) + suite.NoError(err) +} + +func (suite *QueueTestSuite) TestLaterJobExecutionOnShutdownQueue() { + queue := New() + queue.Run() + suite.NoError(queue.Shutdown()) + job := &MockJob{} + err := queue.Later(1, job, []any{"arg1"}) + suite.NoError(err) +} + +func (suite *QueueTestSuite) TestRunQueue() { + queue := New() + job := &MockJob{} + suite.NoError(queue.Push(job, []any{"arg1"})) + queue.Run() + time.Sleep(1 * time.Second) + suite.True(job.Executed) +} + +func (suite *QueueTestSuite) TestRunQueueWithLaterJob() { + queue := New() + job := &MockJob{} + suite.NoError(queue.Later(1, job, []any{"arg1"})) + queue.Run() + time.Sleep(2 * time.Second) + suite.True(job.Executed) +} + +func (suite *QueueTestSuite) TestRunQueueWithBulkJobs() { + queue := New() + jobs := []Jobs{ + {Job: &MockJob{}, Args: []any{"arg1"}}, + {Job: &MockJob{}, Args: []any{"arg2"}}, + } + suite.NoError(queue.Bulk(jobs)) + queue.Run() + time.Sleep(1 * time.Second) +} + +func (suite *QueueTestSuite) TestRunQueueWithErrHandle() { + queue := New() + job := &MockJob{} + suite.NoError(queue.Push(job, []any{"arg1"})) + queue.Run() + time.Sleep(1 * time.Second) + suite.Error(job.Err) +} + +func (suite *QueueTestSuite) TestShutdownQueue() { + queue := New() + queue.Run() + err := queue.Shutdown() + suite.NoError(err) +} + +type MockJob struct { + Executed bool + Err error +} + +func (job *MockJob) Handle(args ...any) error { + job.Executed = true + return errors.New("error") +} + +func (job *MockJob) ErrHandle(err error) { + job.Err = err +} diff --git a/pkg/shell/exec.go b/pkg/shell/exec.go index 63e4b641..8c3cf7de 100644 --- a/pkg/shell/exec.go +++ b/pkg/shell/exec.go @@ -9,9 +9,7 @@ import ( "strings" "time" - "github.com/goravel/framework/support" - - "github.com/TheTNB/panel/v2/pkg/slice" + "github.com/TheTNB/panel/pkg/slice" ) // Execf 执行 shell 命令 @@ -52,12 +50,8 @@ func ExecfAsync(shell string, args ...any) error { } go func() { - err := cmd.Wait() - if err != nil { - if support.Env == support.EnvTest { - fmt.Println(err.Error()) - panic(err) - } + if err = cmd.Wait(); err != nil { + fmt.Println(err.Error()) } }() diff --git a/pkg/ssh/ssh.go b/pkg/ssh/ssh.go index 687ce960..be7a35fe 100644 --- a/pkg/ssh/ssh.go +++ b/pkg/ssh/ssh.go @@ -5,7 +5,7 @@ import ( "golang.org/x/crypto/ssh" - "github.com/TheTNB/panel/v2/pkg/io" + "github.com/TheTNB/panel/pkg/io" ) type AuthMethod int8 diff --git a/pkg/systemctl/service.go b/pkg/systemctl/service.go index 401d8143..1f854d53 100644 --- a/pkg/systemctl/service.go +++ b/pkg/systemctl/service.go @@ -5,7 +5,7 @@ import ( "os/exec" "strings" - "github.com/TheTNB/panel/v2/pkg/shell" + "github.com/TheTNB/panel/pkg/shell" ) // Status 获取服务状态 diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index 74cbd3b9..6216feb6 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -9,9 +9,8 @@ import ( "time" "github.com/go-resty/resty/v2" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/color" - "github.com/goravel/framework/support/env" + "github.com/golang-module/carbon/v2" + "github.com/gookit/color" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/disk" "github.com/shirou/gopsutil/host" @@ -20,8 +19,9 @@ import ( "github.com/shirou/gopsutil/net" "github.com/spf13/cast" - "github.com/TheTNB/panel/v2/pkg/io" - "github.com/TheTNB/panel/v2/pkg/shell" + "github.com/TheTNB/panel/pkg/arch" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/shell" ) // MonitoringInfo 监控信息 @@ -159,70 +159,70 @@ func GetLatestPanelVersion() (PanelInfo, error) { var name, version, body, date, downloadName, downloadUrl, checksums, checksumsUrl string if isChina { - if name, err = shell.Execf("jq -r '.name' " + fileName); err != nil { + if name, err = shell.Execf("jq -r '.name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if version, err = shell.Execf("jq -r '.tag_name' " + fileName); err != nil { + if version, err = shell.Execf("jq -r '.tag_name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if body, err = shell.Execf("jq -r '.description' " + fileName); err != nil { + if body, err = shell.Execf("jq -r '.description' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if date, err = shell.Execf("jq -r '.created_at' " + fileName); err != nil { + if date, err = shell.Execf("jq -r '.created_at' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if checksums, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .name' " + fileName); err != nil { + if checksums, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if checksumsUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .direct_asset_url' " + fileName); err != nil { + if checksumsUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if env.IsArm() { - if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .name' " + fileName); err != nil { + if arch.IsArm() { + if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .direct_asset_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } } else { - if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .name' " + fileName); err != nil { + if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .direct_asset_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } } } else { - if name, err = shell.Execf("jq -r '.name' " + fileName); err != nil { + if name, err = shell.Execf("jq -r '.name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if version, err = shell.Execf("jq -r '.tag_name' " + fileName); err != nil { + if version, err = shell.Execf("jq -r '.tag_name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if body, err = shell.Execf("jq -r '.body' " + fileName); err != nil { + if body, err = shell.Execf("jq -r '.body' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if date, err = shell.Execf("jq -r '.published_at' " + fileName); err != nil { + if date, err = shell.Execf("jq -r '.published_at' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if checksums, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .name' " + fileName); err != nil { + if checksums, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if checksumsUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .browser_download_url' " + fileName); err != nil { + if checksumsUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if env.IsArm() { - if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .name' " + fileName); err != nil { + if arch.IsArm() { + if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .browser_download_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } } else { - if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .name' " + fileName); err != nil { + if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .name' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取最新版本失败") } } @@ -252,9 +252,9 @@ func GetPanelVersion(version string) (PanelInfo, error) { } if isChina { - output, err = shell.Execf(`curl -sSL "https://git.haozi.net/api/v4/projects/opensource%%2Fpanel/releases/` + version + `"`) + output, err = shell.Execf(`curl -sSL "https://git.haozi.net/api/v4/projects/opensource%%2Fpanel/releases/%s"`, version) } else { - output, err = shell.Execf(`curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/tags/` + version + `"`) + output, err = shell.Execf(`curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/tags/%s"`, version) } if len(output) == 0 || err != nil { @@ -278,70 +278,70 @@ func GetPanelVersion(version string) (PanelInfo, error) { var name, version2, body, date, downloadName, downloadUrl, checksums, checksumsUrl string if isChina { - if name, err = shell.Execf("jq -r '.name' " + fileName); err != nil { + if name, err = shell.Execf("jq -r '.name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if version2, err = shell.Execf("jq -r '.tag_name' " + fileName); err != nil { + if version2, err = shell.Execf("jq -r '.tag_name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if body, err = shell.Execf("jq -r '.description' " + fileName); err != nil { + if body, err = shell.Execf("jq -r '.description' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if date, err = shell.Execf("jq -r '.created_at' " + fileName); err != nil { + if date, err = shell.Execf("jq -r '.created_at' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if checksums, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .name' " + fileName); err != nil { + if checksums, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if checksumsUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .direct_asset_url' " + fileName); err != nil { + if checksumsUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"checksums\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if env.IsArm() { - if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .name' " + fileName); err != nil { + if arch.IsArm() { + if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .direct_asset_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"arm64\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } } else { - if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .name' " + fileName); err != nil { + if downloadName, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .direct_asset_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets.links[] | select(.name | contains(\"amd64v2\")) | .direct_asset_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } } } else { - if name, err = shell.Execf("jq -r '.name' " + fileName); err != nil { + if name, err = shell.Execf("jq -r '.name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if version2, err = shell.Execf("jq -r '.tag_name' " + fileName); err != nil { + if version2, err = shell.Execf("jq -r '.tag_name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if body, err = shell.Execf("jq -r '.body' " + fileName); err != nil { + if body, err = shell.Execf("jq -r '.body' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if date, err = shell.Execf("jq -r '.published_at' " + fileName); err != nil { + if date, err = shell.Execf("jq -r '.published_at' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if checksums, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .name' " + fileName); err != nil { + if checksums, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if checksumsUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .browser_download_url' " + fileName); err != nil { + if checksumsUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"checksums\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if env.IsArm() { - if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .name' " + fileName); err != nil { + if arch.IsArm() { + if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .browser_download_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"arm64\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } } else { - if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .name' " + fileName); err != nil { + if downloadName, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .name' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } - if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' " + fileName); err != nil { + if downloadUrl, err = shell.Execf("jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' %s", fileName); err != nil { return info, errors.New("获取面板版本失败") } } @@ -361,113 +361,113 @@ func GetPanelVersion(version string) (PanelInfo, error) { // UpdatePanel 更新面板 func UpdatePanel(panelInfo PanelInfo) error { - color.Green().Printfln("目标版本: " + panelInfo.Version) - color.Green().Printfln("下载链接: " + panelInfo.DownloadUrl) + color.Greenln("目标版本: %s", panelInfo.Version) + color.Greenln("下载链接: %s", panelInfo.DownloadUrl) - color.Green().Printfln("前置检查...") + color.Greenln("前置检查...") if io.Exists("/tmp/panel-storage.zip") || io.Exists("/tmp/panel.conf.bak") { return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败导致的,请谨慎排除后重试") } - color.Green().Printfln("备份面板数据...") + color.Greenln("备份面板数据...") // 备份面板 - if err := io.Archive([]string{"/www/panel"}, "/www/backup/panel/panel-"+carbon.Now().ToShortDateTimeString()+".zip"); err != nil { - color.Red().Printfln("备份面板失败") + if err := io.Compress([]string{"/www/panel"}, fmt.Sprintf("/www/backup/panel/panel-%s.zip", carbon.Now().ToShortDateTimeString()), io.Zip); err != nil { + color.Redln("备份面板失败") return err } if _, err := shell.Execf("cd /www/panel/storage && zip -r /tmp/panel-storage.zip *"); err != nil { - color.Red().Printfln("备份面板数据失败") + color.Redln("备份面板数据失败") return err } if _, err := shell.Execf("cp -f /www/panel/panel.conf /tmp/panel.conf.bak"); err != nil { - color.Red().Printfln("备份面板配置失败") + color.Redln("备份面板配置失败") return err } if !io.Exists("/tmp/panel-storage.zip") || !io.Exists("/tmp/panel.conf.bak") { return errors.New("备份面板数据失败") } - color.Green().Printfln("备份完成") + color.Greenln("备份完成") - color.Green().Printfln("清理旧版本...") + color.Greenln("清理旧版本...") if _, err := shell.Execf("rm -rf /www/panel/*"); err != nil { - color.Red().Printfln("清理旧版本失败") + color.Redln("清理旧版本失败") return err } - color.Green().Printfln("清理完成") + color.Greenln("清理完成") - color.Green().Printfln("正在下载...") - if _, err := shell.Execf("wget -T 120 -t 3 -O /www/panel/" + panelInfo.DownloadName + " " + panelInfo.DownloadUrl); err != nil { - color.Red().Printfln("下载失败") + color.Greenln("正在下载...") + if _, err := shell.Execf("wget -T 120 -t 3 -O /www/panel/%s %s", panelInfo.DownloadName, panelInfo.DownloadUrl); err != nil { + color.Redln("下载失败") return err } - if _, err := shell.Execf("wget -T 20 -t 3 -O /www/panel/" + panelInfo.Checksums + " " + panelInfo.ChecksumsUrl); err != nil { - color.Red().Printfln("下载失败") + if _, err := shell.Execf("wget -T 20 -t 3 -O /www/panel/%s %s", panelInfo.Checksums, panelInfo.ChecksumsUrl); err != nil { + color.Redln("下载失败") return err } if !io.Exists("/www/panel/"+panelInfo.DownloadName) || !io.Exists("/www/panel/"+panelInfo.Checksums) { return errors.New("下载失败") } - color.Green().Printfln("下载完成") + color.Greenln("下载完成") - color.Green().Printfln("校验下载文件...") - check, err := shell.Execf("cd /www/panel && sha256sum -c " + panelInfo.Checksums + " --ignore-missing") + color.Greenln("校验下载文件...") + check, err := shell.Execf("cd /www/panel && sha256sum -c %s", panelInfo.Checksums+" --ignore-missing") if check != panelInfo.DownloadName+": OK" || err != nil { return errors.New("下载文件校验失败") } if err = io.Remove("/www/panel/" + panelInfo.Checksums); err != nil { - color.Red().Printfln("清理临时文件失败") + color.Redln("清理临时文件失败") return err } - color.Green().Printfln("文件校验完成") + color.Greenln("文件校验完成") - color.Green().Printfln("更新新版本...") - if _, err = shell.Execf("cd /www/panel && unzip -o " + panelInfo.DownloadName + " && rm -rf " + panelInfo.DownloadName); err != nil { - color.Red().Printfln("更新失败") + color.Greenln("更新新版本...") + if _, err = shell.Execf("cd /www/panel && unzip -o %s && rm -rf %s", panelInfo.DownloadName, panelInfo.DownloadName); err != nil { + color.Redln("更新失败") return err } if !io.Exists("/www/panel/panel") { return errors.New("更新失败,可能是下载过程中出现了问题") } - color.Green().Printfln("更新完成") + color.Greenln("更新完成") - color.Green().Printfln("恢复面板数据...") + color.Greenln("恢复面板数据...") if _, err = shell.Execf("cp -f /tmp/panel-storage.zip /www/panel/storage/panel-storage.zip && cd /www/panel/storage && unzip -o panel-storage.zip && rm -rf panel-storage.zip"); err != nil { - color.Red().Printfln("恢复面板数据失败") + color.Redln("恢复面板数据失败") return err } if _, err = shell.Execf("cp -f /tmp/panel.conf.bak /www/panel/panel.conf"); err != nil { - color.Red().Printfln("恢复面板配置失败") + color.Redln("恢复面板配置失败") return err } if _, err = shell.Execf("cp -f /www/panel/scripts/panel.sh /usr/bin/panel"); err != nil { - color.Red().Printfln("恢复面板脚本失败") + color.Redln("恢复面板脚本失败") return err } if !io.Exists("/www/panel/storage/panel.db") || !io.Exists("/www/panel/panel.conf") { return errors.New("恢复面板数据失败") } - color.Green().Printfln("恢复完成") + color.Greenln("恢复完成") - color.Green().Printfln("设置面板文件权限...") + color.Greenln("设置面板文件权限...") _, _ = shell.Execf("chmod -R 700 /www/panel") _, _ = shell.Execf("chmod -R 700 /usr/bin/panel") - color.Green().Printfln("设置完成") + color.Greenln("设置完成") - color.Green().Printfln("运行升级后脚本...") + color.Greenln("运行升级后脚本...") if _, err = shell.Execf("bash /www/panel/scripts/update_panel.sh"); err != nil { - color.Red().Printfln("运行面板升级后脚本失败") + color.Redln("运行面板升级后脚本失败") return err } if _, err = shell.Execf("cp -f /www/panel/scripts/panel.service /etc/systemd/system/panel.service"); err != nil { - color.Red().Printfln("写入面板服务文件失败") + color.Redln("写入面板服务文件失败") return err } _, _ = shell.Execf("systemctl daemon-reload") - if _, err = shell.Execf("panel writeSetting version " + panelInfo.Version); err != nil { - color.Red().Printfln("写入面板版本号失败") + if _, err = shell.Execf("panel writeSetting version %s", panelInfo.Version); err != nil { + color.Redln("写入面板版本号失败") return err } - color.Green().Printfln("升级完成") + color.Greenln("升级完成") _, _ = shell.Execf("rm -rf /tmp/panel-storage.zip") _, _ = shell.Execf("rm -rf /tmp/panel.conf.bak") @@ -476,14 +476,14 @@ func UpdatePanel(panelInfo PanelInfo) error { } func RestartPanel() { - color.Green().Printfln("重启面板...") + color.Greenln("重启面板...") err := shell.ExecfAsync("sleep 2 && systemctl restart panel") if err != nil { - color.Red().Printfln("重启失败") + color.Redln("重启失败") return } - color.Green().Printfln("重启完成") + color.Greenln("重启完成") } // IsChina 是否中国大陆 diff --git a/pkg/tools/tools_test.go b/pkg/tools/tools_test.go index 3a89e0bf..42bc32c5 100644 --- a/pkg/tools/tools_test.go +++ b/pkg/tools/tools_test.go @@ -48,11 +48,11 @@ func (s *HelperTestSuite) TestGenerateVersions() { }, versions) } -func (s *HelperTestSuite) TestGetLatestPanelVersion() { +/*func (s *HelperTestSuite) TestGetLatestPanelVersion() { version, err := GetLatestPanelVersion() s.NotEmpty(version) s.Nil(err) -} +}*/ func (s *HelperTestSuite) TestGetPanelVersion() { version, err := GetPanelVersion("v2.1.29") diff --git a/pkg/types/common.go b/pkg/types/common.go index 7939130f..33c98941 100644 --- a/pkg/types/common.go +++ b/pkg/types/common.go @@ -9,3 +9,28 @@ type KV struct { Key string `json:"key"` Value string `json:"value"` } + +type LV struct { + Label string `json:"label"` + Value string `json:"value"` +} + +// KVToMap 将 key-value 切片转换为 map +func KVToMap(kvs []KV) map[string]string { + m := make(map[string]string) + for _, item := range kvs { + m[item.Key] = item.Value + } + + return m +} + +// KVToSlice 将 key-value 切片转换为 key=value 切片 +func KVToSlice(kvs []KV) []string { + var s []string + for _, item := range kvs { + s = append(s, item.Key+"="+item.Value) + } + + return s +} diff --git a/pkg/types/plugin.go b/pkg/types/plugin.go index f664012e..beb0c285 100644 --- a/pkg/types/plugin.go +++ b/pkg/types/plugin.go @@ -1,17 +1,17 @@ package types -import "github.com/goravel/framework/contracts/foundation" +import "github.com/go-chi/chi/v5" // Plugin 插件元数据结构 type Plugin struct { - Name string // 插件名称 - Description string // 插件描述 - Slug string // 插件标识 - Version string // 插件版本 - Requires []string // 依赖插件 - Excludes []string // 排除插件 - Install string // 安装命令 - Uninstall string // 卸载命令 - Update string // 更新命令 - Boot func(app foundation.Application) // 启动时执行的命令 + Slug string `json:"slug"` // 插件标识 + Name string `json:"name"` // 插件名称 + Description string `json:"description"` // 插件描述 + Version string `json:"version"` // 插件版本 + Requires []string `json:"requires"` // 依赖插件 + Excludes []string `json:"excludes"` // 排除插件 + Install string `json:"-"` // 安装命令 + Uninstall string `json:"-"` // 卸载命令 + Update string `json:"-"` // 更新命令 + Route func(r chi.Router) `json:"-"` // 路由 } diff --git a/renovate.json b/renovate.json index 9b1b5d7e..2a877874 100644 --- a/renovate.json +++ b/renovate.json @@ -7,21 +7,26 @@ "🤖 Dependencies" ], "commitMessagePrefix": "chore(deps): ", + "lockFileMaintenance": { + "enabled": true, + "automerge": true + }, "postUpdateOptions": [ - "gomodTidy" + "gomodTidy", + "pnpmDedupe" ], "packageRules": [ { - "description": "Automerge non-major updates", + "description": "Automerge updates", "matchUpdateTypes": [ + "major", "minor", "patch" ], - "matchCurrentVersion": "!/^0/", "automerge": true } ], "ignoreDeps": [ - "github.com/goravel/framework" + "eslint" ] } diff --git a/routes/api.go b/routes/api.go deleted file mode 100644 index b7a66f8a..00000000 --- a/routes/api.go +++ /dev/null @@ -1,234 +0,0 @@ -package routes - -import ( - "path/filepath" - - "github.com/goravel/framework/contracts/http" - "github.com/goravel/framework/contracts/route" - "github.com/goravel/framework/facades" - frameworkmiddleware "github.com/goravel/framework/http/middleware" - - "github.com/TheTNB/panel/v2/app/http/controllers" - "github.com/TheTNB/panel/v2/app/http/middleware" - "github.com/TheTNB/panel/v2/embed" -) - -func Api() { - facades.Route().Prefix("api/panel").Group(func(r route.Router) { - r.Prefix("info").Group(func(r route.Router) { - infoController := controllers.NewInfoController() - r.Get("panel", infoController.Panel) - r.Middleware(middleware.Session()).Get("homePlugins", infoController.HomePlugins) - r.Middleware(middleware.Session()).Get("nowMonitor", infoController.NowMonitor) - r.Middleware(middleware.Session()).Get("systemInfo", infoController.SystemInfo) - r.Middleware(middleware.Session()).Get("countInfo", infoController.CountInfo) - r.Middleware(middleware.Session()).Get("installedDbAndPhp", infoController.InstalledDbAndPhp) - r.Middleware(middleware.Session()).Get("checkUpdate", infoController.CheckUpdate) - r.Middleware(middleware.Session()).Get("updateInfo", infoController.UpdateInfo) - r.Middleware(middleware.Session()).Post("update", infoController.Update) - r.Middleware(middleware.Session()).Post("restart", infoController.Restart) - }) - r.Prefix("user").Group(func(r route.Router) { - userController := controllers.NewUserController() - r.Middleware(frameworkmiddleware.Throttle("login")).Post("login", userController.Login) - r.Post("logout", userController.Logout) - r.Get("isLogin", userController.IsLogin) - r.Middleware(middleware.Session()).Get("info", userController.Info) - }) - r.Prefix("task").Middleware(middleware.Session()).Group(func(r route.Router) { - taskController := controllers.NewTaskController() - r.Get("status", taskController.Status) - r.Get("list", taskController.List) - r.Get("log", taskController.Log) - r.Post("delete", taskController.Delete) - }) - r.Prefix("website").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - websiteController := controllers.NewWebsiteController() - r.Get("defaultConfig", websiteController.GetDefaultConfig) - r.Post("defaultConfig", websiteController.SaveDefaultConfig) - r.Get("backupList", websiteController.BackupList) - r.Put("uploadBackup", websiteController.UploadBackup) - r.Delete("deleteBackup", websiteController.DeleteBackup) - }) - r.Prefix("websites").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - websiteController := controllers.NewWebsiteController() - r.Get("/", websiteController.List) - r.Post("/", websiteController.Add) - r.Post("delete", websiteController.Delete) - r.Get("{id}/config", websiteController.GetConfig) - r.Post("{id}/config", websiteController.SaveConfig) - r.Delete("{id}/log", websiteController.ClearLog) - r.Post("{id}/updateRemark", websiteController.UpdateRemark) - r.Post("{id}/createBackup", websiteController.CreateBackup) - r.Post("{id}/restoreBackup", websiteController.RestoreBackup) - r.Post("{id}/resetConfig", websiteController.ResetConfig) - r.Post("{id}/status", websiteController.Status) - }) - r.Prefix("cert").Middleware(middleware.Session()).Group(func(r route.Router) { - certController := controllers.NewCertController() - r.Get("caProviders", certController.CAProviders) - r.Get("dnsProviders", certController.DNSProviders) - r.Get("algorithms", certController.Algorithms) - r.Get("users", certController.UserList) - r.Post("users", certController.UserStore) - r.Put("users/{id}", certController.UserUpdate) - r.Get("users/{id}", certController.UserShow) - r.Delete("users/{id}", certController.UserDestroy) - r.Get("dns", certController.DNSList) - r.Post("dns", certController.DNSStore) - r.Put("dns/{id}", certController.DNSUpdate) - r.Get("dns/{id}", certController.DNSShow) - r.Delete("dns/{id}", certController.DNSDestroy) - r.Get("certs", certController.CertList) - r.Post("certs", certController.CertStore) - r.Put("certs/{id}", certController.CertUpdate) - r.Get("certs/{id}", certController.CertShow) - r.Delete("certs/{id}", certController.CertDestroy) - r.Post("obtain", certController.Obtain) - r.Post("renew", certController.Renew) - r.Post("manualDNS", certController.ManualDNS) - r.Post("deploy", certController.Deploy) - }) - r.Prefix("plugin").Middleware(middleware.Session()).Group(func(r route.Router) { - pluginController := controllers.NewPluginController() - r.Get("list", pluginController.List) - r.Post("install", pluginController.Install) - r.Post("uninstall", pluginController.Uninstall) - r.Post("update", pluginController.Update) - r.Post("updateShow", pluginController.UpdateShow) - r.Get("isInstalled", pluginController.IsInstalled) - }) - r.Prefix("cron").Middleware(middleware.Session()).Group(func(r route.Router) { - cronController := controllers.NewCronController() - r.Get("list", cronController.List) - r.Get("{id}", cronController.Script) - r.Post("add", cronController.Add) - r.Put("{id}", cronController.Update) - r.Delete("{id}", cronController.Delete) - r.Post("status", cronController.Status) - r.Get("log/{id}", cronController.Log) - }) - r.Prefix("safe").Middleware(middleware.Session()).Group(func(r route.Router) { - safeController := controllers.NewSafeController() - r.Get("firewallStatus", safeController.GetFirewallStatus) - r.Post("firewallStatus", safeController.SetFirewallStatus) - r.Get("firewallRules", safeController.GetFirewallRules) - r.Post("firewallRules", safeController.AddFirewallRule) - r.Delete("firewallRules", safeController.DeleteFirewallRule) - r.Get("sshStatus", safeController.GetSshStatus) - r.Post("sshStatus", safeController.SetSshStatus) - r.Get("sshPort", safeController.GetSshPort) - r.Post("sshPort", safeController.SetSshPort) - r.Get("pingStatus", safeController.GetPingStatus) - r.Post("pingStatus", safeController.SetPingStatus) - }) - r.Prefix("container").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - containerController := controllers.NewContainerController() - r.Get("list", containerController.ContainerList) - r.Get("search", containerController.ContainerSearch) - r.Post("create", containerController.ContainerCreate) - r.Post("remove", containerController.ContainerRemove) - r.Post("start", containerController.ContainerStart) - r.Post("stop", containerController.ContainerStop) - r.Post("restart", containerController.ContainerRestart) - r.Post("pause", containerController.ContainerPause) - r.Post("unpause", containerController.ContainerUnpause) - r.Get("inspect", containerController.ContainerInspect) - r.Post("kill", containerController.ContainerKill) - r.Post("rename", containerController.ContainerRename) - r.Get("stats", containerController.ContainerStats) - r.Get("exist", containerController.ContainerExist) - r.Get("logs", containerController.ContainerLogs) - r.Post("prune", containerController.ContainerPrune) - - r.Prefix("network").Group(func(r route.Router) { - r.Get("list", containerController.NetworkList) - r.Post("create", containerController.NetworkCreate) - r.Post("remove", containerController.NetworkRemove) - r.Get("exist", containerController.NetworkExist) - r.Get("inspect", containerController.NetworkInspect) - r.Post("connect", containerController.NetworkConnect) - r.Post("disconnect", containerController.NetworkDisconnect) - r.Post("prune", containerController.NetworkPrune) - }) - - r.Prefix("image").Group(func(r route.Router) { - r.Get("list", containerController.ImageList) - r.Get("exist", containerController.ImageExist) - r.Post("pull", containerController.ImagePull) - r.Post("remove", containerController.ImageRemove) - r.Get("inspect", containerController.ImageInspect) - r.Post("prune", containerController.ImagePrune) - }) - - r.Prefix("volume").Group(func(r route.Router) { - r.Get("list", containerController.VolumeList) - r.Post("create", containerController.VolumeCreate) - r.Get("exist", containerController.VolumeExist) - r.Get("inspect", containerController.VolumeInspect) - r.Post("remove", containerController.VolumeRemove) - r.Post("prune", containerController.VolumePrune) - }) - }) - r.Prefix("file").Middleware(middleware.Session()).Group(func(r route.Router) { - fileController := controllers.NewFileController() - r.Post("create", fileController.Create) - r.Get("content", fileController.Content) - r.Post("save", fileController.Save) - r.Post("delete", fileController.Delete) - r.Post("upload", fileController.Upload) - r.Post("move", fileController.Move) - r.Post("copy", fileController.Copy) - r.Get("download", fileController.Download) - r.Post("remoteDownload", fileController.RemoteDownload) - r.Get("info", fileController.Info) - r.Post("permission", fileController.Permission) - r.Post("archive", fileController.Archive) - r.Post("unArchive", fileController.UnArchive) - r.Post("search", fileController.Search) - r.Get("list", fileController.List) - }) - r.Prefix("monitor").Middleware(middleware.Session()).Group(func(r route.Router) { - monitorController := controllers.NewMonitorController() - r.Post("switch", monitorController.Switch) - r.Post("saveDays", monitorController.SaveDays) - r.Post("clear", monitorController.Clear) - r.Get("list", monitorController.List) - r.Get("switchAndDays", monitorController.SwitchAndDays) - }) - r.Prefix("ssh").Middleware(middleware.Session()).Group(func(r route.Router) { - sshController := controllers.NewSshController() - r.Get("info", sshController.GetInfo) - r.Post("info", sshController.UpdateInfo) - r.Get("session", sshController.Session) - }) - r.Prefix("setting").Middleware(middleware.Session()).Group(func(r route.Router) { - settingController := controllers.NewSettingController() - r.Get("list", settingController.List) - r.Post("update", settingController.Update) - r.Get("https", settingController.GetHttps) - r.Post("https", settingController.UpdateHttps) - }) - r.Prefix("system").Middleware(middleware.Session()).Group(func(r route.Router) { - controller := controllers.NewSystemController() - r.Get("service/status", controller.ServiceStatus) - r.Get("service/isEnabled", controller.ServiceIsEnabled) - r.Post("service/enable", controller.ServiceEnable) - r.Post("service/disable", controller.ServiceDisable) - r.Post("service/restart", controller.ServiceRestart) - r.Post("service/reload", controller.ServiceReload) - r.Post("service/start", controller.ServiceStart) - r.Post("service/stop", controller.ServiceStop) - }) - }) - - // 文档 - swaggerController := controllers.NewSwaggerController() - facades.Route().Middleware(middleware.Session()).Get("swagger/*any", swaggerController.Index) - - // 404 - facades.Route().Fallback(func(ctx http.Context) http.Response { - index, _ := embed.PublicFS.ReadFile(filepath.Join("frontend", "index.html")) - return ctx.Response().Data(http.StatusOK, "text/html; charset=utf-8", index) - }) -} diff --git a/routes/plugin.go b/routes/plugin.go deleted file mode 100644 index 2ef1a6f3..00000000 --- a/routes/plugin.go +++ /dev/null @@ -1,172 +0,0 @@ -package routes - -import ( - "github.com/goravel/framework/contracts/route" - "github.com/goravel/framework/facades" - - "github.com/TheTNB/panel/v2/app/http/controllers/plugins" - "github.com/TheTNB/panel/v2/app/http/middleware" -) - -// Plugin 加载插件路由 -func Plugin() { - facades.Route().Prefix("api/plugins").Middleware(middleware.Session(), middleware.MustInstall()).Group(func(r route.Router) { - r.Prefix("mysql").Group(func(route route.Router) { - mySQLController := plugins.NewMySQLController() - route.Get("load", mySQLController.Load) - route.Get("config", mySQLController.GetConfig) - route.Post("config", mySQLController.SaveConfig) - route.Get("errorLog", mySQLController.ErrorLog) - route.Post("clearErrorLog", mySQLController.ClearErrorLog) - route.Get("slowLog", mySQLController.SlowLog) - route.Post("clearSlowLog", mySQLController.ClearSlowLog) - route.Get("rootPassword", mySQLController.GetRootPassword) - route.Post("rootPassword", mySQLController.SetRootPassword) - route.Get("databases", mySQLController.DatabaseList) - route.Post("databases", mySQLController.AddDatabase) - route.Delete("databases", mySQLController.DeleteDatabase) - route.Get("backups", mySQLController.BackupList) - route.Post("backups", mySQLController.CreateBackup) - route.Put("backups", mySQLController.UploadBackup) - route.Delete("backups", mySQLController.DeleteBackup) - route.Post("backups/restore", mySQLController.RestoreBackup) - route.Get("users", mySQLController.UserList) - route.Post("users", mySQLController.AddUser) - route.Delete("users", mySQLController.DeleteUser) - route.Post("users/password", mySQLController.SetUserPassword) - route.Post("users/privileges", mySQLController.SetUserPrivileges) - }) - r.Prefix("postgresql").Group(func(route route.Router) { - controller := plugins.NewPostgreSQLController() - route.Get("load", controller.Load) - route.Get("config", controller.GetConfig) - route.Post("config", controller.SaveConfig) - route.Get("userConfig", controller.GetUserConfig) - route.Post("userConfig", controller.SaveUserConfig) - route.Get("log", controller.Log) - route.Post("clearLog", controller.ClearLog) - route.Get("databases", controller.DatabaseList) - route.Post("databases", controller.AddDatabase) - route.Delete("databases", controller.DeleteDatabase) - route.Get("backups", controller.BackupList) - route.Post("backups", controller.CreateBackup) - route.Put("backups", controller.UploadBackup) - route.Delete("backups", controller.DeleteBackup) - route.Post("backups/restore", controller.RestoreBackup) - route.Get("roles", controller.RoleList) - route.Post("roles", controller.AddRole) - route.Delete("roles", controller.DeleteRole) - route.Post("roles/password", controller.SetRolePassword) - }) - r.Prefix("php").Group(func(route route.Router) { - phpController := plugins.NewPHPController() - route.Get("{version}/load", phpController.Load) - route.Get("{version}/config", phpController.GetConfig) - route.Post("{version}/config", phpController.SaveConfig) - route.Get("{version}/fpmConfig", phpController.GetFPMConfig) - route.Post("{version}/fpmConfig", phpController.SaveFPMConfig) - route.Get("{version}/errorLog", phpController.ErrorLog) - route.Get("{version}/slowLog", phpController.SlowLog) - route.Post("{version}/clearErrorLog", phpController.ClearErrorLog) - route.Post("{version}/clearSlowLog", phpController.ClearSlowLog) - route.Get("{version}/extensions", phpController.ExtensionList) - route.Post("{version}/extensions", phpController.InstallExtension) - route.Delete("{version}/extensions", phpController.UninstallExtension) - }) - r.Prefix("phpmyadmin").Group(func(route route.Router) { - phpMyAdminController := plugins.NewPhpMyAdminController() - route.Get("info", phpMyAdminController.Info) - route.Post("port", phpMyAdminController.SetPort) - route.Get("config", phpMyAdminController.GetConfig) - route.Post("config", phpMyAdminController.SaveConfig) - }) - r.Prefix("pureftpd").Group(func(route route.Router) { - pureFtpdController := plugins.NewPureFtpdController() - route.Get("list", pureFtpdController.List) - route.Post("add", pureFtpdController.Add) - route.Delete("delete", pureFtpdController.Delete) - route.Post("changePassword", pureFtpdController.ChangePassword) - route.Get("port", pureFtpdController.GetPort) - route.Post("port", pureFtpdController.SetPort) - }) - r.Prefix("redis").Group(func(route route.Router) { - redisController := plugins.NewRedisController() - route.Get("load", redisController.Load) - route.Get("config", redisController.GetConfig) - route.Post("config", redisController.SaveConfig) - }) - r.Prefix("s3fs").Group(func(route route.Router) { - s3fsController := plugins.NewS3fsController() - route.Get("list", s3fsController.List) - route.Post("add", s3fsController.Add) - route.Post("delete", s3fsController.Delete) - }) - r.Prefix("supervisor").Group(func(route route.Router) { - supervisorController := plugins.NewSupervisorController() - route.Get("service", supervisorController.Service) - route.Get("log", supervisorController.Log) - route.Post("clearLog", supervisorController.ClearLog) - route.Get("config", supervisorController.Config) - route.Post("config", supervisorController.SaveConfig) - route.Get("processes", supervisorController.Processes) - route.Post("startProcess", supervisorController.StartProcess) - route.Post("stopProcess", supervisorController.StopProcess) - route.Post("restartProcess", supervisorController.RestartProcess) - route.Get("processLog", supervisorController.ProcessLog) - route.Post("clearProcessLog", supervisorController.ClearProcessLog) - route.Get("processConfig", supervisorController.ProcessConfig) - route.Post("processConfig", supervisorController.SaveProcessConfig) - route.Post("deleteProcess", supervisorController.DeleteProcess) - route.Post("addProcess", supervisorController.AddProcess) - - }) - r.Prefix("fail2ban").Group(func(route route.Router) { - fail2banController := plugins.NewFail2banController() - route.Get("jails", fail2banController.List) - route.Post("jails", fail2banController.Add) - route.Delete("jails", fail2banController.Delete) - route.Get("jails/{name}", fail2banController.BanList) - route.Post("unban", fail2banController.Unban) - route.Post("whiteList", fail2banController.SetWhiteList) - route.Get("whiteList", fail2banController.GetWhiteList) - }) - r.Prefix("podman").Group(func(route route.Router) { - controller := plugins.NewPodmanController() - route.Get("registryConfig", controller.GetRegistryConfig) - route.Post("registryConfig", controller.UpdateRegistryConfig) - route.Get("storageConfig", controller.GetStorageConfig) - route.Post("storageConfig", controller.UpdateStorageConfig) - }) - r.Prefix("rsync").Group(func(route route.Router) { - rsyncController := plugins.NewRsyncController() - route.Get("modules", rsyncController.List) - route.Post("modules", rsyncController.Create) - route.Post("modules/{name}", rsyncController.Update) - route.Delete("modules/{name}", rsyncController.Destroy) - route.Get("config", rsyncController.GetConfig) - route.Post("config", rsyncController.UpdateConfig) - }) - r.Prefix("frp").Group(func(route route.Router) { - frpController := plugins.NewFrpController() - route.Get("config", frpController.GetConfig) - route.Post("config", frpController.UpdateConfig) - }) - r.Prefix("gitea").Group(func(route route.Router) { - giteaController := plugins.NewGiteaController() - route.Get("config", giteaController.GetConfig) - route.Post("config", giteaController.UpdateConfig) - }) - r.Prefix("toolbox").Group(func(route route.Router) { - toolboxController := plugins.NewToolBoxController() - route.Get("dns", toolboxController.GetDNS) - route.Post("dns", toolboxController.SetDNS) - route.Get("swap", toolboxController.GetSWAP) - route.Post("swap", toolboxController.SetSWAP) - route.Get("timezone", toolboxController.GetTimezone) - route.Post("timezone", toolboxController.SetTimezone) - route.Get("hosts", toolboxController.GetHosts) - route.Post("hosts", toolboxController.SetHosts) - route.Post("rootPassword", toolboxController.SetRootPassword) - }) - }) -} diff --git a/scripts/calculate_j.sh b/scripts/calculate_j.sh deleted file mode 100644 index e165f0d0..00000000 --- a/scripts/calculate_j.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -# 计算 j 值(通用) -calculate_j() { - export LC_ALL=C - mem=$(free -m | awk '/^Mem:/{print $2}') - swap=$(free -m | awk '/^Swap:/{print $2}') - total=$((mem + swap)) - j_value=$((total / 1024)) - cpu_cores=$(nproc) - - if [ $j_value -eq 0 ]; then - j_value=1 - fi - - if [ $j_value -gt "$cpu_cores" ]; then - j_value=$cpu_cores - fi - - echo "$j_value" -} - -# 计算 j 值(2倍内存) -calculate_j2() { - export LC_ALL=C - mem=$(free -m | awk '/^Mem:/{print $2}') - swap=$(free -m | awk '/^Swap:/{print $2}') - total=$((mem + swap)) - j_value=$((total / 2024)) - cpu_cores=$(nproc) - - if [ $j_value -eq 0 ]; then - j_value=1 - fi - - if [ $j_value -gt "$cpu_cores" ]; then - j_value=$cpu_cores - fi - - echo "$j_value" -} diff --git a/scripts/fail2ban/install.sh b/scripts/fail2ban/install.sh deleted file mode 100644 index 0aacfbc0..00000000 --- a/scripts/fail2ban/install.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf install -y fail2ban -elif [ "${OS}" == "debian" ]; then - apt-get install -y fail2ban -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:fail2ban安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 修改 fail2ban 配置文件 -sed -i 's!# logtarget.*!logtarget = /var/log/fail2ban.log!' /etc/fail2ban/fail2ban.conf -sed -i 's!logtarget\s*=.*!logtarget = /var/log/fail2ban.log!' /etc/fail2ban/jail.conf -cat > /etc/fail2ban/jail.local << EOF -[DEFAULT] -ignoreip = 127.0.0.1/8 -bantime = 600 -findtime = 300 -maxretry = 5 -banaction = firewallcmd-ipset -action = %(action_mwl)s - -# ssh-START -[ssh] -enabled = true -filter = sshd -port = 22 -maxretry = 5 -findtime = 300 -bantime = 86400 -action = %(action_mwl)s -logpath = /var/log/secure -# ssh-END -EOF -# 替换端口 -sshPort=$(cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}') -if [ "${sshPort}" == "" ]; then - sshPort="22" -fi -sed -i "s/port = 22/port = ${sshPort}/g" /etc/fail2ban/jail.local - -# Debian 的特殊处理 -if [ "${OS}" == "debian" ]; then - sed -i "s/\/var\/log\/secure/\/var\/log\/auth.log/g" /etc/fail2ban/jail.local - sed -i "s/banaction = firewallcmd-ipset/banaction = ufw/g" /etc/fail2ban/jail.local -fi - -# 启动 fail2ban -systemctl daemon-reload -systemctl unmask fail2ban -systemctl enable fail2ban -systemctl start fail2ban - -panel writePlugin fail2ban 1.0.2 diff --git a/scripts/fail2ban/uninstall.sh b/scripts/fail2ban/uninstall.sh deleted file mode 100644 index 2ea75013..00000000 --- a/scripts/fail2ban/uninstall.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -fail2ban-client unban --all -fail2ban-client stop -systemctl stop fail2ban -systemctl disable fail2ban - -if [ "${OS}" == "centos" ]; then - dnf remove -y fail2ban -elif [ "${OS}" == "debian" ]; then - apt-get purge -y fail2ban -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -rm -rf /etc/fail2ban - -panel deletePlugin fail2ban diff --git a/scripts/fail2ban/update.sh b/scripts/fail2ban/update.sh deleted file mode 100644 index ec9b57fe..00000000 --- a/scripts/fail2ban/update.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf update -y fail2ban -elif [ "${OS}" == "debian" ]; then - apt-get install --only-upgrade -y fail2ban -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:fail2ban安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -systemctl restart fail2ban - -panel writePlugin fail2ban 1.0.2 diff --git a/scripts/frp/install.sh b/scripts/frp/install.sh deleted file mode 100644 index 7a391336..00000000 --- a/scripts/frp/install.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -downloadUrl="https://dl.cdn.haozi.net/panel/frp" -frpPath="/www/server/frp" -frpVersion="0.58.0" - -if [ ! -d "${frpPath}" ]; then - mkdir -p ${frpPath} -fi - -# 架构判断 -if [ "${ARCH}" == "x86_64" ]; then - frpFile="frp_${frpVersion}_linux_amd64.7z" -elif [ "${ARCH}" == "aarch64" ]; then - frpFile="frp_${frpVersion}_linux_arm64.7z" -else - echo -e $HR - echo "错误:不支持的架构" - exit 1 -fi - -# 下载frp -cd ${frpPath} -wget -T 120 -t 3 -O ${frpPath}/${frpFile} ${downloadUrl}/${frpFile} -wget -T 20 -t 3 -O ${frpPath}/${frpFile}.checksum.txt ${downloadUrl}/${frpFile}.checksum.txt -if ! sha256sum --status -c ${frpPath}/${frpFile}.checksum.txt; then - echo -e $HR - echo "错误:frp checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${frpPath} - exit 1 -fi - -# 解压frp -cd ${frpPath} -7z x ${frpFile} -chmod -R 700 ${frpPath} -rm -f ${frpFile} ${frpFile}.checksum.txt - -# 配置systemd -cat >/etc/systemd/system/frps.service </etc/systemd/system/frpc.service <. -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -downloadUrl="https://dl.cdn.haozi.net/panel/frp" -frpPath="/www/server/frp" -frpVersion="0.58.0" - -if [ ! -d "${frpPath}" ]; then - mkdir -p ${frpPath} -fi - -# 架构判断 -if [ "${ARCH}" == "x86_64" ]; then - frpFile="frp_${frpVersion}_linux_amd64.7z" -elif [ "${ARCH}" == "aarch64" ]; then - frpFile="frp_${frpVersion}_linux_arm64.7z" -else - echo -e $HR - echo "错误:不支持的架构" - exit 1 -fi - -# 备份配置 -if [ -f "${frpPath}/frps.toml" ]; then - cp -f ${frpPath}/frps.toml ${frpPath}/frps.toml.bak -fi -if [ -f "${frpPath}/frpc.toml" ]; then - cp -f ${frpPath}/frpc.toml ${frpPath}/frpc.toml.bak -fi - -# 下载frp -cd ${frpPath} -wget -T 120 -t 3 -O ${frpPath}/${frpFile} ${downloadUrl}/${frpFile} -wget -T 20 -t 3 -O ${frpPath}/${frpFile}.checksum.txt ${downloadUrl}/${frpFile}.checksum.txt -if ! sha256sum --status -c ${frpPath}/${frpFile}.checksum.txt; then - echo -e $HR - echo "错误:frp checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${frpPath} - exit 1 -fi - -# 解压frp -cd ${frpPath} -7z x ${frpFile} -chmod -R 700 ${frpPath} -rm -f ${frpFile} ${frpFile}.checksum.txt - -# 还原配置 -if [ -f "${frpPath}/frps.toml.bak" ]; then - cp -f ${frpPath}/frps.toml.bak ${frpPath}/frps.toml -fi -if [ -f "${frpPath}/frpc.toml.bak" ]; then - cp -f ${frpPath}/frpc.toml.bak ${frpPath}/frpc.toml -fi - -systemctl restart frps -systemctl restart frpc - -panel writePlugin frp ${frpVersion} -echo -e ${HR} -echo "frp 安装完成" -echo -e ${HR} - diff --git a/scripts/gitea/install.sh b/scripts/gitea/install.sh deleted file mode 100644 index dc83a02c..00000000 --- a/scripts/gitea/install.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/gitea" -giteaPath="/www/server/gitea" -giteaVersion="1.22.0" - -if [ ! -d "${giteaPath}" ]; then - mkdir -p ${giteaPath} -fi - -# 架构判断 -if [ "${ARCH}" == "x86_64" ]; then - giteaFile="gitea-${giteaVersion}-linux-amd64.7z" -elif [ "${ARCH}" == "aarch64" ]; then - giteaFile="gitea-${giteaVersion}-linux-arm64.7z" -else - echo -e $HR - echo "错误:不支持的架构" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf install git git-lfs -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install git git-lfs -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -git lfs install -git lfs version - -# 下载 -cd ${giteaPath} -wget -T 120 -t 3 -O ${giteaPath}/${giteaFile} ${downloadUrl}/${giteaFile} -wget -T 20 -t 3 -O ${giteaPath}/${giteaFile}.checksum.txt ${downloadUrl}/${giteaFile}.checksum.txt -if ! sha256sum --status -c ${giteaPath}/${giteaFile}.checksum.txt; then - echo -e $HR - echo "错误:gitea checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${giteaPath} - exit 1 -fi - -# 解压 -cd ${giteaPath} -7z x ${giteaFile} -rm -f ${giteaFile} ${giteaFile}.checksum.txt -mv gitea-${giteaVersion}-linux-* gitea -if [ ! -f "${giteaPath}/gitea" ]; then - echo -e $HR - echo "错误:gitea 解压失败" - rm -rf ${giteaPath} - exit 1 -fi - -# 初始化目录 -mkdir -p ${giteaPath}/{custom,data,log} -chown -R www:www ${giteaPath} -chmod -R 750 ${giteaPath} -ln -sf ${giteaPath}/gitea /usr/local/bin/gitea - -# 配置systemd -cat >/etc/systemd/system/gitea.service <. -' - -HR="+----------------------------------------------------" -giteaPath="/www/server/gitea" - -systemctl stop gitea -systemctl disable gitea - -rm -f /usr/local/bin/gitea -rm -rf ${giteaPath} -rm -f /etc/systemd/system/gitea.service -systemctl daemon-reload - -panel deletePlugin gitea -echo -e $HR -echo "gitea 卸载完成,数据库可能需自行删除" -echo -e $HR diff --git a/scripts/gitea/update.sh b/scripts/gitea/update.sh deleted file mode 100644 index 6aa748d1..00000000 --- a/scripts/gitea/update.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -downloadUrl="https://dl.cdn.haozi.net/panel/gitea" -giteaPath="/www/server/gitea" -giteaVersion="1.22.0" - -# 架构判断 -if [ "${ARCH}" == "x86_64" ]; then - giteaFile="gitea-${giteaVersion}-linux-amd64.7z" -elif [ "${ARCH}" == "aarch64" ]; then - giteaFile="gitea-${giteaVersion}-linux-arm64.7z" -else - echo -e $HR - echo "错误:不支持的架构" - exit 1 -fi - -# 下载 -cd ${giteaPath} -wget -T 120 -t 3 -O ${giteaPath}/${giteaFile} ${downloadUrl}/${giteaFile} -wget -T 20 -t 3 -O ${giteaPath}/${giteaFile}.checksum.txt ${downloadUrl}/${giteaFile}.checksum.txt -if ! sha256sum --status -c ${giteaPath}/${giteaFile}.checksum.txt; then - echo -e $HR - echo "错误:gitea checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${giteaPath} - exit 1 -fi - -# 解压 -cd ${giteaPath} -7z x ${giteaFile} -rm -f ${giteaFile} ${giteaFile}.checksum.txt - -# 替换文件 -systemctl stop gitea -rm -f gitea -mv gitea-${giteaVersion}-linux-* gitea -if [ ! -f "${giteaPath}/gitea" ]; then - echo -e $HR - echo "错误:gitea 解压失败" - rm -rf ${giteaPath} - exit 1 -fi - -chown -R www:www ${giteaPath} -chmod -R 750 ${giteaPath} -systemctl start gitea - -panel writePlugin gitea ${giteaVersion} -echo -e $HR -echo "gitea 升级完成" -echo -e $HR diff --git a/scripts/install_panel.sh b/scripts/install_panel.sh deleted file mode 100644 index 55c114c2..00000000 --- a/scripts/install_panel.sh +++ /dev/null @@ -1,357 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -LOGO="+----------------------------------------------------\n| 耗子面板安装脚本\n+----------------------------------------------------\n| Copyright © 2022-"$(date +%Y)" 耗子科技 All rights reserved.\n+----------------------------------------------------" -HR="+----------------------------------------------------" -setup_Path="/www" -current_Path=$(pwd) -sshPort=$(cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}') -inChina=$(curl --retry 2 -m 10 -L https://www.cloudflare-cn.com/cdn-cgi/trace 2> /dev/null | grep -qx 'loc=CN' && echo "true" || echo "false") - -Prepare_System() { - if [ $(whoami) != "root" ]; then - echo -e $HR - echo "错误:请使用 root 用户运行安装命令。" - exit 1 - fi - - ARCH=$(uname -m) - OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - if [ "${OS}" == "unknown" ]; then - echo -e $HR - echo "错误:该系统不支持安装耗子面板,请更换 Debian 12.x / RHEL 9.x 安装。" - exit 1 - fi - if [ "${ARCH}" != "x86_64" ] && [ "${ARCH}" != "aarch64" ]; then - echo -e $HR - echo "错误:该系统架构不支持安装耗子面板,请更换 x86_64 / aarch64 架构安装。" - exit 1 - fi - - if [ "${ARCH}" == "x86_64" ]; then - if [ "$(cat /proc/cpuinfo | grep -c ssse3)" -lt "1" ]; then - echo -e $HR - echo "错误:至少需运行在支持 x86-64-v2 的 CPU 上,请开启对应 CPU 指令集后重试。" - exit 1 - fi - fi - - kernelVersion=$(uname -r | awk -F '.' '{print $1}') - if [ "${kernelVersion}" != "5" ] && [ "${kernelVersion}" != "6" ]; then - echo -e $HR - echo "错误:该系统内核版本太低,不支持安装耗子面板,请更换 Debian 12 / RHEL 9.x 安装。" - exit 1 - fi - - is64bit=$(getconf LONG_BIT) - if [ "${is64bit}" != '64' ]; then - echo -e $HR - echo "错误:32 位系统不支持安装耗子面板,请更换 64 位系统安装。" - exit 1 - fi - - isInstalled=$(systemctl status panel 2>&1 | grep "Active") - if [ "${isInstalled}" != "" ]; then - echo -e $HR - echo "错误:耗子面板已安装,请勿重复安装。" - exit 1 - fi - - if ! id -u "www" > /dev/null 2>&1; then - groupadd www - useradd -s /sbin/nologin -g www www - fi - - if [ ! -d ${setup_Path} ]; then - mkdir ${setup_Path} - fi - - timedatectl set-timezone Asia/Shanghai - - [ -s /etc/selinux/config ] && sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config - setenforce 0 > /dev/null 2>&1 - - ulimit -n 1048576 - echo 2147483584 > /proc/sys/fs/file-max - checkSoftNofile=$(cat /etc/security/limits.conf | grep '^* soft nofile .*$') - checkHardNofile=$(cat /etc/security/limits.conf | grep '^* hard nofile .*$') - checkSoftNproc=$(cat /etc/security/limits.conf | grep '^* soft nproc .*$') - checkHardNproc=$(cat /etc/security/limits.conf | grep '^* hard nproc .*$') - checkFsFileMax=$(cat /etc/sysctl.conf | grep '^fs.file-max.*$') - if [ "${checkSoftNofile}" == "" ]; then - echo "* soft nofile 1048576" >> /etc/security/limits.conf - fi - if [ "${checkHardNofile}" == "" ]; then - echo "* hard nofile 1048576" >> /etc/security/limits.conf - fi - if [ "${checkSoftNproc}" == "" ]; then - echo "* soft nproc 1048576" >> /etc/security/limits.conf - fi - if [ "${checkHardNproc}" == "" ]; then - echo "* hard nproc 1048576" >> /etc/security/limits.conf - fi - if [ "${checkFsFileMax}" == "" ]; then - echo fs.file-max = 2147483584 >> /etc/sysctl.conf - fi - - # 自动开启 BBR - bbrSupported=$(ls -l /lib/modules/*/kernel/net/ipv4 | grep -c tcp_bbr) - bbrEnabled=$(sysctl net.ipv4.tcp_congestion_control | grep -c bbr) - if [ "${bbrSupported}" != "0" ] && [ "${bbrEnabled}" == "0" ]; then - qdisc=$(sysctl net.core.default_qdisc | awk '{print $3}') - echo "net.core.default_qdisc=${qdisc}" >> /etc/sysctl.conf - echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf - sysctl -p - fi - - if [ "${OS}" == "centos" ]; then - if ${inChina}; then - sed -e 's|^mirrorlist=|#mirrorlist=|g' \ - -e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.tencent.com/rocky|g' \ - -e 's|^# baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.tencent.com/rocky|g' \ - -i.bak \ - /etc/yum.repos.d/[Rr]ocky*.repo - sed -e 's|^mirrorlist=|#mirrorlist=|g' \ - -e 's|^#baseurl=https://repo.almalinux.org|baseurl=https://mirrors.tencent.com|g' \ - -e 's|^# baseurl=https://repo.almalinux.org|baseurl=https://mirrors.tencent.com|g' \ - -i.bak \ - /etc/yum.repos.d/[Aa]lmalinux*.repo - - dnf makecache -y - fi - dnf install dnf-plugins-core -y - dnf install epel-release -y - dnf config-manager --set-enabled epel - if ${inChina}; then - sed -i 's|^#baseurl=https://download.example/pub|baseurl=https://mirrors.tencent.com|' /etc/yum.repos.d/epel* - sed -i 's|^# baseurl=https://download.example/pub|baseurl=https://mirrors.tencent.com|' /etc/yum.repos.d/epel* - sed -i 's|^metalink|#metalink|' /etc/yum.repos.d/epel* - dnf makecache -y - fi - # EL 8 - dnf config-manager --set-enabled powertools - # EL 9 - dnf config-manager --set-enabled crb - # Rocky Linux - /usr/bin/crb enable - dnf makecache -y - dnf install -y bash curl wget zip unzip tar p7zip p7zip-plugins git jq git-core dos2unix rsyslog make - elif [ "${OS}" == "debian" ]; then - if ${inChina}; then - sed -i 's/deb.debian.org/mirrors.tencent.com/g' /etc/apt/sources.list - sed -i 's/security.debian.org/mirrors.tencent.com/g' /etc/apt/sources.list - fi - apt-get update -y - apt-get install -y bash curl wget zip unzip tar p7zip p7zip-full git jq git dos2unix rsyslog make - fi - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装面板依赖软件失败,请截图错误信息寻求帮助。" - exit 1 - fi - - systemctl enable rsyslog - systemctl start rsyslog - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装面板依赖软件失败,请截图错误信息寻求帮助。" - exit 1 - fi -} - -Auto_Swap() { - # 判断是否有swap - swap=$(LC_ALL=C free | grep Swap | awk '{print $2}') - if [ "${swap}" -gt 1 ]; then - return - fi - - # 设置swap - swapFile="${setup_Path}/swap" - btrfsCheck=$(df -T /www | awk '{print $2}' | tail -n 1) - if [ "${btrfsCheck}" == "btrfs" ]; then - btrfs filesystem mkswapfile --size 4G --uuid clear ${swapFile} - else - dd if=/dev/zero of=$swapFile bs=1M count=4096 - fi - chmod 600 $swapFile - mkswap -f $swapFile - swapon $swapFile - echo "$swapFile swap swap defaults 0 0" >> /etc/fstab - - mount -a - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:检测到系统的 /etc/fstab 文件配置有误,请检查排除后重试,问题解决前勿重启系统。" - exit 1 - fi -} - -Init_Panel() { - systemctl stop panel - systemctl disable panel - rm -f /etc/systemd/system/panel.service - rm -rf ${setup_Path}/panel - mkdir ${setup_Path}/server - mkdir ${setup_Path}/server/cron - mkdir ${setup_Path}/server/cron/logs - chmod -R 755 ${setup_Path}/server - mkdir ${setup_Path}/panel - # 下载面板zip包并解压 - if [ "${ARCH}" == "x86_64" ]; then - if ${inChina}; then - panelZip=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("amd64v2")) | .direct_asset_url') - panelZipName=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("amd64v2")) | .name') - else - panelZip=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("amd64v2")) | .browser_download_url') - panelZipName=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("amd64v2")) | .name') - fi - elif [ "${ARCH}" == "aarch64" ]; then - if ${inChina}; then - panelZip=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("arm64")) | .direct_asset_url') - panelZipName=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("arm64")) | .name') - else - panelZip=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("arm64")) | .browser_download_url') - panelZipName=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("arm64")) | .name') - fi - else - echo -e $HR - echo "错误:该系统架构不支持安装耗子面板,请更换 x86_64 / aarch64 架构安装。" - exit 1 - fi - if [ "$?" != "0" ] || [ "${panelZip}" == "" ] || [ "${panelZipName}" == "" ]; then - echo -e $HR - echo "错误:获取面板下载链接失败,请截图错误信息寻求帮助。" - exit 1 - fi - wget -T 120 -t 3 -O ${setup_Path}/panel/${panelZipName} "${panelZip}" - - # 下载 checksums 文件 - if ${inChina}; then - checksumsFile=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("checksums")) | .direct_asset_url') - checksumsFileName=$(curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel/releases/permalink/latest" | jq -r '.assets.links[] | select(.name | contains("checksums")) | .name') - else - checksumsFile=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("checksums")) | .browser_download_url') - checksumsFileName=$(curl -sSL "https://api.github.com/repos/TheTNB/panel/releases/latest" | jq -r '.assets[] | select(.name | contains("checksums")) | .name') - fi - wget -T 20 -t 3 -O ${setup_Path}/panel/${checksumsFileName} "${checksumsFile}" - - cd ${setup_Path}/panel - if ! sha256sum --status -c ${checksumsFileName} --ignore-missing; then - echo -e $HR - echo "错误:面板压缩包 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - unzip -o ${panelZipName} - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:解压面板失败,请截图错误信息寻求帮助。" - exit 1 - fi - rm -rf ${panelZipName} - rm -rf ${checksumsFileName} - cp panel-example.conf panel.conf - - # 设置面板 - entrance=$(cat /dev/urandom | head -n 16 | md5sum | head -c 6) - sed -i "s!APP_ENTRANCE=.*!APP_ENTRANCE=/${entrance}!g" panel.conf - ${setup_Path}/panel/panel --env="panel.conf" artisan key:generate - ${setup_Path}/panel/panel --env="panel.conf" artisan jwt:secret - openssl req -x509 -nodes -days 36500 -newkey ec:<(openssl ecparam -name secp384r1) -keyout ${setup_Path}/panel/storage/ssl.key -out ${setup_Path}/panel/storage/ssl.crt -subj "/C=CN/ST=Tianjin/L=Tianjin/O=HaoZi Technology Co., Ltd./OU=HaoZi Panel/CN=Panel" - chmod -R 700 ${setup_Path}/panel - cp -f scripts/panel.sh /usr/bin/panel - chmod -R 700 /usr/bin/panel - # 防火墙放行 - if [ "${OS}" == "centos" ]; then - dnf install firewalld -y - systemctl enable firewalld - systemctl start firewalld - firewall-cmd --set-default-zone=public > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=22/tcp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=80/tcp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=443/tcp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=443/udp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=8888/tcp > /dev/null 2>&1 - firewall-cmd --permanent --zone=public --add-port=${sshPort}/tcp > /dev/null 2>&1 - firewall-cmd --reload - elif [ "${OS}" == "debian" ]; then - apt-get install ufw -y - echo y | ufw enable - systemctl enable ufw - systemctl start ufw - ufw allow 22/tcp - ufw allow 80/tcp - ufw allow 443/tcp - ufw allow 443/udp - ufw allow 8888/tcp - ufw allow ${sshPort}/tcp - ufw reload - fi - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:防火墙放行失败,请截图错误信息寻求帮助。" - exit 1 - fi - # 写入服务文件 - cp -f ${setup_Path}/panel/scripts/panel.service /etc/systemd/system/panel.service - systemctl daemon-reload - systemctl enable panel.service - systemctl start panel.service - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:面板启动失败,请截图错误信息寻求帮助。" - exit 1 - fi - - clear - echo -e $LOGO - echo '面板安装成功!' - echo -e $HR - panel init - panel getInfo - - cd ${current_Path} - rm -f install_panel.sh - rm -f install_panel.sh.checksum.txt -} - -clear -echo -e $LOGO - -# 安装确认 -read -p "面板将安装至 ${setup_Path} 目录,请输入 y 并回车以开始安装:" install -if [ "$install" != 'y' ]; then - echo "输入不正确,已退出安装。" - exit -fi - -clear -echo -e $LOGO -echo '安装面板依赖软件(如报错请检查 APT/Yum 源是否正常)' -echo -e $HR -sleep 2s -Prepare_System -Auto_Swap - -echo -e $LOGO -echo '安装面板运行环境(视网络情况可能需要较长时间)' -echo -e $HR -sleep 2s -Init_Panel diff --git a/scripts/mysql/install.sh b/scripts/mysql/install.sh deleted file mode 100644 index 94eac14a..00000000 --- a/scripts/mysql/install.sh +++ /dev/null @@ -1,352 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}') -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/mysql" -setupPath="/www" -mysqlPath="${setupPath}/server/mysql" -mysqlVersion="" -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -if [[ "${1}" == "84" ]]; then - mysqlVersion="8.4.0" - j=$(calculate_j2) -elif [[ "${1}" == "80" ]]; then - mysqlVersion="8.0.37" - j=$(calculate_j2) -elif [[ "${1}" == "57" ]]; then - mysqlVersion="5.7.44" -else - echo -e $HR - echo "错误:不支持的 MySQL 版本!" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install cmake bison ncurses-devel libtirpc-devel openssl-devel pkg-config openldap-devel libudev-devel cyrus-sasl-devel patchelf rpcgen rpcsvc-proto-devel -y - dnf install gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils gcc-toolset-12-annobin-annocheck gcc-toolset-12-annobin-plugin-gcc -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential cmake bison libncurses5-dev libtirpc-dev libssl-dev pkg-config libldap2-dev libudev-dev libsasl2-dev patchelf -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -mysqlUserCheck=$(cat /etc/passwd | grep mysql) -if [ "${mysqlUserCheck}" == "" ]; then - groupadd mysql - useradd -s /sbin/nologin -g mysql mysql -fi - -# 准备目录 -rm -rf ${mysqlPath} -mkdir -p ${mysqlPath} -cd ${mysqlPath} - -# 下载源码 -wget -T 120 -t 3 -O ${mysqlPath}/mysql-${mysqlVersion}.7z ${downloadUrl}/mysql-${mysqlVersion}.7z -wget -T 20 -t 3 -O ${mysqlPath}/mysql-${mysqlVersion}.7z.checksum.txt ${downloadUrl}/mysql-${mysqlVersion}.7z.checksum.txt -if ! sha256sum --status -c mysql-${mysqlVersion}.7z.checksum.txt; then - echo -e $HR - echo "错误:MySQL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${mysqlPath} - exit 1 -fi - -7z x mysql-${mysqlVersion}.7z -rm -f mysql-${mysqlVersion}.7z -rm -f mysql-${mysqlVersion}.7z.checksum.txt - -# 编译 -mv mysql-${mysqlVersion} src -chmod -R 755 src -cd src -rm mysql-test/CMakeLists.txt -sed -i 's/ADD_SUBDIRECTORY(mysql-test)//g' CMakeLists.txt -mkdir build -cd build - -# 5.7 和 8.0 需要 boost -if [[ "${1}" == "57" ]] || [[ "${1}" == "80" ]]; then - MAYBE_WITH_BOOST="-DWITH_BOOST=../boost" -fi - -cmake .. -DCMAKE_INSTALL_PREFIX=${mysqlPath} -DMYSQL_DATADIR=${mysqlPath}/data -DSYSCONFDIR=${mysqlPath}/conf -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_ARCHIVE_STORAGE_ENGINE=1 -DWITH_FEDERATED_STORAGE_ENGINE=1 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 -DDEFAULT_CHARSET=utf8mb4 -DDEFAULT_COLLATION=utf8mb4_general_ci -DENABLED_LOCAL_INFILE=1 -DWITH_DEBUG=0 -DWITH_UNIT_TESTS=OFF -DINSTALL_MYSQLTESTDIR= -DCMAKE_BUILD_TYPE=Release -DWITH_SYSTEMD=1 -DSYSTEMD_PID_DIR=${mysqlPath} ${MAYBE_WITH_BOOST} -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 编译初始化失败,请截图错误信息寻求帮助。" - rm -rf ${mysqlPath} - exit 1 -fi - -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 编译失败,请截图错误信息寻求帮助。" - rm -rf ${mysqlPath} - exit 1 -fi - -# 安装 -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 安装失败,请截图错误信息寻求帮助。" - rm -rf ${mysqlPath} - exit 1 -fi - -# 配置 -mkdir ${mysqlPath}/conf -cat > ${mysqlPath}/conf/my.cnf << EOF -[client] -port = 3306 -socket = /tmp/mysql.sock - -[mysqld] -port = 3306 -socket = /tmp/mysql.sock -datadir = ${mysqlPath}/data -default_storage_engine = InnoDB -skip-external-locking -table_definition_cache = 400 -performance_schema_max_table_instances = 400 -key_buffer_size = 8M -max_allowed_packet = 1G -table_open_cache = 32 -sort_buffer_size = 256K -net_buffer_length = 4K -read_buffer_size = 128K -read_rnd_buffer_size = 256K -myisam_sort_buffer_size = 4M -thread_cache_size = 4 -query_cache_size = 4M -tmp_table_size = 8M -explicit_defaults_for_timestamp = 1 -#skip-name-resolve -max_connections = 500 -max_connect_errors = 100 -open_files_limit = 65535 -early-plugin-load = "" - -log-bin = mysql-bin -server-id = 1 -slow_query_log = 1 -slow-query-log-file = ${mysqlPath}/mysql-slow.log -long_query_time = 3 -log-error = ${mysqlPath}/mysql-error.log - -innodb_data_home_dir = ${mysqlPath}/data -innodb_data_file_path = ibdata1:10M:autoextend -innodb_log_group_home_dir = ${mysqlPath}/data -innodb_buffer_pool_size = 16M -innodb_redo_log_capacity = 5M -innodb_log_buffer_size = 8M -innodb_flush_log_at_trx_commit = 1 -innodb_lock_wait_timeout = 50 -innodb_max_dirty_pages_pct = 90 -innodb_read_io_threads = 4 -innodb_write_io_threads = 4 - -[mysqldump] -quick -max_allowed_packet = 500M - -[myisamchk] -key_buffer_size = 20M -sort_buffer_size = 20M -read_buffer = 2M -write_buffer = 2M - -[mysqlhotcopy] -interactive-timeout -EOF - -# 根据CPU核心数确定写入线程数 -sed -i 's/innodb_write_io_threads = 4/innodb_write_io_threads = '${cpuCore}'/g' ${mysqlPath}/conf/my.cnf -sed -i 's/innodb_read_io_threads = 4/innodb_read_io_threads = '${cpuCore}'/g' ${mysqlPath}/conf/my.cnf - -if [[ "${1}" == "84" ]] || [[ "${1}" == "80" ]]; then - sed -i '/query_cache_size/d' ${mysqlPath}/conf/my.cnf -fi -if [[ "${1}" == "57" ]]; then - sed -i '/innodb_redo_log_capacity/d' ${mysqlPath}/conf/my.cnf -fi - -# 根据内存大小调参 -if [[ ${memTotal} -gt 1024 && ${memTotal} -lt 2048 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 128#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 768K#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 768K#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 16#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 16M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 32M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 2048 && ${memTotal} -lt 4096 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 256#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 1M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 1M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 32#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 32M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 4096 && ${memTotal} -lt 8192 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 512#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 2M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 2M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 64#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 8192 && ${memTotal} -lt 16384 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 1024#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 4M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 4M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 128#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 1024M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 16384 && ${memTotal} -lt 32768 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 2048#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 256#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 2048M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 1G#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf -elif [[ ${memTotal} -ge 32768 ]]; then - sed -i "s#^key_buffer_size.*#key_buffer_size = 1024M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^table_open_cache.*#table_open_cache = 4096#" ${mysqlPath}/conf/my.cnf - sed -i "s#^sort_buffer_size.*#sort_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^read_buffer_size.*#read_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^thread_cache_size.*#thread_cache_size = 512#" ${mysqlPath}/conf/my.cnf - sed -i "s#^query_cache_size.*#query_cache_size = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^tmp_table_size.*#tmp_table_size = 512M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 4096M#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 2G#" ${mysqlPath}/conf/my.cnf - sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 512M#" ${mysqlPath}/conf/my.cnf -fi - -# 初始化 -cd ${mysqlPath} -rm -rf ${mysqlPath}/src -rm -rf ${mysqlPath}/data -mkdir -p ${mysqlPath}/data -chown -R mysql:mysql ${mysqlPath} -chmod -R 755 ${mysqlPath} -chmod 644 ${mysqlPath}/conf/my.cnf - -${mysqlPath}/bin/mysqld --initialize-insecure --user=mysql --basedir=${mysqlPath} --datadir=${mysqlPath}/data - -# 软链接 -ln -sf ${mysqlPath}/bin/* /usr/bin/ - -# 写入 systemd 配置 -cat > /etc/systemd/system/mysqld.service << EOF -[Unit] -Description=MySQL Server -Documentation=http://dev.mysql.com/doc/refman/en/using-systemd.html -After=network.target -After=syslog.target - -[Install] -WantedBy=multi-user.target - -[Service] -User=mysql -Group=mysql - -Type=forking - -PIDFile=/www/server/mysql/mysqld.pid - -# Disable service start and stop timeout logic of systemd for mysqld service. -TimeoutSec=0 - -# Execute pre and post scripts as root -PermissionsStartOnly=true - -# Start main service -ExecStart=/www/server/mysql/bin/mysqld --daemonize --pid-file=/www/server/mysql/mysqld.pid \$MYSQLD_OPTS - -# Use this to switch malloc implementation -EnvironmentFile=-/etc/sysconfig/mysql - -# Sets open_files_limit -LimitNOFILE = 500000 - -Restart=on-failure - -RestartPreventExitStatus=1 - -PrivateTmp=false -EOF - -systemctl daemon-reload -systemctl enable mysqld -systemctl start mysqld - -mysqlPassword=$(cat /dev/urandom | head -n 16 | md5sum | head -c 16) -${mysqlPath}/bin/mysqladmin -u root password ${mysqlPassword} -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "DROP DATABASE test;" -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "DELETE FROM mysql.user WHERE user='';" -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "FLUSH PRIVILEGES;" - -panel writePlugin mysql${1} ${mysqlVersion} -panel writeMysqlPassword ${mysqlPassword} - -echo -e "${HR}\nMySQL-${1} 安装完成\n${HR}" diff --git a/scripts/mysql/uninstall.sh b/scripts/mysql/uninstall.sh deleted file mode 100644 index 67845204..00000000 --- a/scripts/mysql/uninstall.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -systemctl stop mysqld -systemctl disable mysqld -rm -rf /etc/systemd/system/mysqld.service -systemctl daemon-reload -pkill -9 mysqld -rm -rf /www/server/mysql - -rm -f /usr/bin/mysql* -rm -f /usr/lib/libmysql* -rm -f /usr/lib64/libmysql* - -userdel -r mysql -groupdel mysql - -panel deletePlugin mysql${1} - -echo -e "${HR}\nMySQL-${1} 卸载完成\n${HR}" diff --git a/scripts/mysql/update.sh b/scripts/mysql/update.sh deleted file mode 100644 index aa1d127f..00000000 --- a/scripts/mysql/update.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}') -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/mysql" -setupPath="/www" -mysqlPath="${setupPath}/server/mysql" -mysqlVersion="" -mysqlPassword=$(panel getSetting mysql_root_password) -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -if [[ "${1}" == "84" ]]; then - mysqlVersion="8.4.0" - j=$(calculate_j2) -elif [[ "${1}" == "80" ]]; then - mysqlVersion="8.0.37" - j=$(calculate_j2) -elif [[ "${1}" == "57" ]]; then - mysqlVersion="5.7.44" -else - echo -e $HR - echo "错误:不支持的 MySQL 版本!" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install cmake bison ncurses-devel libtirpc-devel openssl-devel pkg-config openldap-devel libudev-devel cyrus-sasl-devel patchelf rpcgen rpcsvc-proto-devel p7zip p7zip-plugins -y - dnf install gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils gcc-toolset-12-annobin-annocheck gcc-toolset-12-annobin-plugin-gcc -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential cmake bison libncurses5-dev libtirpc-dev libssl-dev pkg-config libldap2-dev libudev-dev libsasl2-dev patchelf p7zip p7zip-full -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -# 停止已有服务 -systemctl stop mysqld - -# 准备目录 -cd ${mysqlPath} - -# 下载源码 -wget -T 120 -t 3 -O ${mysqlPath}/mysql-${mysqlVersion}.7z ${downloadUrl}/mysql-${mysqlVersion}.7z -wget -T 20 -t 3 -O ${mysqlPath}/mysql-${mysqlVersion}.7z.checksum.txt ${downloadUrl}/mysql-${mysqlVersion}.7z.checksum.txt -if ! sha256sum --status -c mysql-${mysqlVersion}.7z.checksum.txt; then - echo -e $HR - echo "错误:MySQL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${mysqlPath} - exit 1 -fi - -7z x mysql-${mysqlVersion}.7z -rm -f mysql-${mysqlVersion}.7z -rm -f mysql-${mysqlVersion}.7z.checksum.txt - -# 编译 -mv mysql-${mysqlVersion} src -chmod -R 755 src -cd src -mkdir build -cd build - -# 5.7 需要 boost -if [[ "${1}" == "57" ]]; then - MAYBE_WITH_BOOST="-DWITH_BOOST=../boost" -fi - -cmake .. -DCMAKE_INSTALL_PREFIX=${mysqlPath} -DMYSQL_DATADIR=${mysqlPath}/data -DSYSCONFDIR=${mysqlPath}/conf -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_ARCHIVE_STORAGE_ENGINE=1 -DWITH_FEDERATED_STORAGE_ENGINE=1 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 -DDEFAULT_CHARSET=utf8mb4 -DDEFAULT_COLLATION=utf8mb4_general_ci -DENABLED_LOCAL_INFILE=1 -DWITH_DEBUG=0 -DWITH_UNIT_TESTS=OFF -DINSTALL_MYSQLTESTDIR= -DCMAKE_BUILD_TYPE=Release -DWITH_SYSTEMD=1 -DSYSTEMD_PID_DIR=${mysqlPath} ${MAYBE_WITH_BOOST} -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 编译初始化失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 编译失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 安装 -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:MySQL 安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 设置权限 -chown -R mysql:mysql ${mysqlPath} -chmod -R 755 ${mysqlPath} -chmod 644 ${mysqlPath}/conf/my.cnf - -# 软链接 -ln -sf ${mysqlPath}/bin/* /usr/bin/ - -# 启动服务 -systemctl start mysqld - -# 执行更新后的初始化 -if [[ "${1}" == "57" ]]; then - ${mysqlPath}/bin/mysql_upgrade -uroot -p${mysqlPassword} -fi -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "DROP DATABASE test;" -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "DELETE FROM mysql.user WHERE user='';" -${mysqlPath}/bin/mysql -uroot -p${mysqlPassword} -e "FLUSH PRIVILEGES;" - -panel writePlugin mysql${1} ${mysqlVersion} - -echo -e "${HR}\nMySQL-${1} 升级完成\n${HR}" diff --git a/scripts/openresty/install.sh b/scripts/openresty/install.sh deleted file mode 100644 index 70f10c50..00000000 --- a/scripts/openresty/install.sh +++ /dev/null @@ -1,685 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel" -setupPath="/www" -openrestyPath="${setupPath}/server/openresty" -openrestyVersion="1.25.3.1" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install cmake tar unzip gd gd-devel git-core flex perl oniguruma oniguruma-devel libsodium-devel libxml2-devel libxslt-devel bison yajl yajl-devel curl curl-devel ncurses-devel libevent-devel readline-devel libuuid-devel brotli-devel icu libicu libicu-devel openssl openssl-devel -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential cmake tar unzip libgd3 libgd-dev git flex perl libonig-dev libsodium-dev libxml2-dev libxslt1-dev bison libyajl-dev curl libcurl4-openssl-dev libncurses5-dev libevent-dev libreadline-dev uuid-dev libbrotli-dev icu-devtools libicu-dev openssl libssl-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 准备目录 -rm -rf ${openrestyPath} -mkdir -p ${openrestyPath} -cd ${openrestyPath} - -# 下载源码 -wget -T 120 -t 3 -O ${openrestyPath}/openresty-${openrestyVersion}.tar.gz ${downloadUrl}/openresty/openresty-${openrestyVersion}.tar.gz -wget -T 20 -t 3 -O ${openrestyPath}/openresty-${openrestyVersion}.tar.gz.checksum.txt ${downloadUrl}/openresty/openresty-${openrestyVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c openresty-${openrestyVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:OpenResty 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -tar -zxvf openresty-${openrestyVersion}.tar.gz -rm -f openresty-${openrestyVersion}.tar.gz -rm -f openresty-${openrestyVersion}.tar.gz.checksum.txt -mv openresty-${openrestyVersion} src -cd src - -# tls library -wget -T 120 -t 3 -O quictls-1.1.1w.7z ${downloadUrl}/tls/quictls-1.1.1w.7z -wget -T 20 -t 3 -O quictls-1.1.1w.7z.checksum.txt ${downloadUrl}/tls/quictls-1.1.1w.7z.checksum.txt - -if ! sha256sum --status -c quictls-1.1.1w.7z.checksum.txt; then - echo -e $HR - echo "错误:quictls 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -7z x quictls-1.1.1w.7z -rm -f quictls-1.1.1w.7z -rm -f quictls-1.1.1w.7z.checksum.txt -mv quictls-1.1.1w quictls -chmod -R 755 quictls - -# patch tls library -cd quictls -wget -T 20 -t 3 -O openssl-1.1.1f-sess_set_get_cb_yield.patch ${downloadUrl}/openresty/openssl/openssl-1.1.1f-sess_set_get_cb_yield.patch -wget -T 20 -t 3 -O openssl-1.1.1f-sess_set_get_cb_yield.patch.checksum.txt ${downloadUrl}/openresty/openssl/openssl-1.1.1f-sess_set_get_cb_yield.patch.checksum.txt - -if ! sha256sum --status -c openssl-1.1.1f-sess_set_get_cb_yield.patch.checksum.txt; then - echo -e $HR - echo "错误:OpenSSL 补丁文件 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -patch -p1 < openssl-1.1.1f-sess_set_get_cb_yield.patch -rm -f openssl-1.1.1f-sess_set_get_cb_yield.patch -rm -f openssl-1.1.1f-sess_set_get_cb_yield.patch.checksum.txt -cd ../ - -# pcre2 -wget -T 60 -t 3 -O pcre2-10.43.7z ${downloadUrl}/openresty/pcre/pcre2-10.43.7z -wget -T 20 -t 3 -O pcre2-10.43.7z.checksum.txt ${downloadUrl}/openresty/pcre/pcre2-10.43.7z.checksum.txt - -if ! sha256sum --status -c pcre2-10.43.7z.checksum.txt; then - echo -e $HR - echo "错误:pcre2 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -7z x pcre2-10.43.7z -rm -f pcre2-10.43.7z -rm -f pcre2-10.43.7z.checksum.txt -mv pcre2-10.43 pcre2 -chmod -R 755 pcre2 - -# ngx_cache_purge -wget -T 20 -t 3 -O ngx_cache_purge-2.3.tar.gz ${downloadUrl}/openresty/modules/ngx_cache_purge-2.3.tar.gz -wget -T 20 -t 3 -O ngx_cache_purge-2.3.tar.gz.checksum.txt ${downloadUrl}/openresty/modules/ngx_cache_purge-2.3.tar.gz.checksum.txt - -if ! sha256sum --status -c ngx_cache_purge-2.3.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:ngx_cache_purge 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -tar -zxvf ngx_cache_purge-2.3.tar.gz -rm -f ngx_cache_purge-2.3.tar.gz -rm -f ngx_cache_purge-2.3.tar.gz.checksum.txt -mv ngx_cache_purge-2.3 ngx_cache_purge - -# nginx-sticky-module -wget -T 20 -t 3 -O nginx-sticky-module.zip ${downloadUrl}/openresty/modules/nginx-sticky-module.zip -wget -T 20 -t 3 -O nginx-sticky-module.zip.checksum.txt ${downloadUrl}/openresty/modules/nginx-sticky-module.zip.checksum.txt - -if ! sha256sum --status -c nginx-sticky-module.zip.checksum.txt; then - echo -e $HR - echo "错误:nginx-sticky-module 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o nginx-sticky-module.zip -rm -f nginx-sticky-module.zip -rm -f nginx-sticky-module.zip.checksum.txt - -# nginx-dav-ext-module -wget -T 20 -t 3 -O nginx-dav-ext-module-3.0.0.tar.gz ${downloadUrl}/openresty/modules/nginx-dav-ext-module-3.0.0.tar.gz -wget -T 20 -t 3 -O nginx-dav-ext-module-3.0.0.tar.gz.checksum.txt ${downloadUrl}/openresty/modules/nginx-dav-ext-module-3.0.0.tar.gz.checksum.txt - -if ! sha256sum --status -c nginx-dav-ext-module-3.0.0.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:nginx-dav-ext-module 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -tar -xvf nginx-dav-ext-module-3.0.0.tar.gz -rm -f nginx-dav-ext-module-3.0.0.tar.gz -rm -f nginx-dav-ext-module-3.0.0.tar.gz.checksum.txt -mv nginx-dav-ext-module-3.0.0 nginx-dav-ext-module - -# waf -wget -T 60 -t 3 -O uthash-2.3.0.zip ${downloadUrl}/openresty/modules/uthash-2.3.0.zip -wget -T 20 -t 3 -O uthash-2.3.0.zip.checksum.txt ${downloadUrl}/openresty/modules/uthash-2.3.0.zip.checksum.txt - -if ! sha256sum --status -c uthash-2.3.0.zip.checksum.txt; then - echo -e $HR - echo "错误:uthash 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o uthash-2.3.0.zip -mv uthash-2.3.0 uthash -rm -f uthash-2.3.0.zip -rm -f uthash-2.3.0.zip.checksum.txt -cd ../ - -wget -T 20 -t 3 -O ngx_waf-6.1.9.zip ${downloadUrl}/openresty/modules/ngx_waf-6.1.9.zip -wget -T 20 -t 3 -O ngx_waf-6.1.9.zip.checksum.txt ${downloadUrl}/openresty/modules/ngx_waf-6.1.9.zip.checksum.txt - -if ! sha256sum --status -c ngx_waf-6.1.9.zip.checksum.txt; then - echo -e $HR - echo "错误:ngx_waf 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o ngx_waf-6.1.9.zip -mv ngx_waf-6.1.9 ngx_waf -rm -f ngx_waf-6.1.9.zip -rm -f ngx_waf-6.1.9.zip.checksum.txt - -cd ngx_waf/inc -wget -T 60 -t 3 -O libinjection-3.10.0.zip ${downloadUrl}/openresty/modules/libinjection-3.10.0.zip -wget -T 20 -t 3 -O libinjection-3.10.0.zip.checksum.txt ${downloadUrl}/openresty/modules/libinjection-3.10.0.zip.checksum.txt - -if ! sha256sum --status -c libinjection-3.10.0.zip.checksum.txt; then - echo -e $HR - echo "错误:libinjection 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o libinjection-3.10.0.zip -mv libinjection-3.10.0 libinjection -rm -f libinjection-3.10.0.zip -rm -f libinjection-3.10.0.zip.checksum.txt - -cd ../ -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:OpenResty waf拓展初始化失败,请截图错误信息寻求帮助。" - rm -rf ${openrestyPath} - exit 1 -fi -cd ${openrestyPath}/src - -# brotli -wget -T 20 -t 3 -O ngx_brotli-a71f931.zip ${downloadUrl}/openresty/modules/ngx_brotli-a71f931.zip -wget -T 20 -t 3 -O ngx_brotli-a71f931.zip.checksum.txt ${downloadUrl}/openresty/modules/ngx_brotli-a71f931.zip.checksum.txt - -if ! sha256sum --status -c ngx_brotli-a71f931.zip.checksum.txt; then - echo -e $HR - echo "错误:ngx_brotli 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${openrestyPath} - exit 1 -fi - -unzip -o ngx_brotli-a71f931.zip -mv ngx_brotli-a71f931 ngx_brotli -rm -f ngx_brotli-a71f931.zip -rm -f ngx_brotli-a71f931.zip.checksum.txt -cd ngx_brotli/deps/brotli -mkdir out && cd out -cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-Ofast -march=native -mtune=native -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_CXX_FLAGS="-Ofast -march=native -mtune=native -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_INSTALL_PREFIX=./installed .. -cmake --build . --config Release --target brotlienc - -cd ${openrestyPath}/src -export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH -export LIB_UTHASH=${openrestyPath}/src/uthash - -# 临时 patch,去除 --without-pcre2 -sed -i '/# disable pcre2 by default/,/push @ngx_opts, '\''--without-pcre2'\'';/d' configure - -./configure --user=www --group=www --prefix=${openrestyPath} --with-luajit --add-module=${openrestyPath}/src/ngx_cache_purge --add-module=${openrestyPath}/src/nginx-sticky-module --with-openssl=${openrestyPath}/src/quictls --with-pcre=${openrestyPath}/src/pcre2 --with-pcre-jit --with-http_v2_module --with-http_v3_module --with-http_slice_module --with-stream --with-stream_ssl_module --with-stream_realip_module --with-stream_ssl_preread_module --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-ipv6 --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-http_auth_request_module --with-http_secure_link_module --with-http_random_index_module --with-ld-opt="-Wl,-s -Wl,-Bsymbolic -Wl,--gc-sections" --with-cc-opt="-DNGX_LUA_ABORT_AT_PANIC -march=native -mtune=native -Ofast -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" --with-luajit-xcflags="-DLUAJIT_NUMMODE=2 -DLUAJIT_ENABLE_LUA52COMPAT" --with-file-aio --with-threads --with-compat --with-http_dav_module --add-module=${openrestyPath}/src/nginx-dav-ext-module --add-module=${openrestyPath}/src/ngx_brotli --add-module=${openrestyPath}/ngx_waf -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:OpenResty编译失败,请截图错误信息寻求帮助。" - rm -rf ${openrestyPath} - exit 1 -fi -make install -if [ ! -f "${openrestyPath}/nginx/sbin/nginx" ]; then - echo -e $HR - echo "错误:OpenResty安装失败,请截图错误信息寻求帮助。" - rm -rf ${openrestyPath} - exit 1 -fi - -# 设置软链接 -ln -sf ${openrestyPath}/nginx/html ${openrestyPath}/html -ln -sf ${openrestyPath}/nginx/conf ${openrestyPath}/conf -ln -sf ${openrestyPath}/nginx/logs ${openrestyPath}/logs -ln -sf ${openrestyPath}/nginx/sbin ${openrestyPath}/sbin -ln -sf ${openrestyPath}/nginx/sbin/nginx /usr/bin/openresty -rm -f ${openrestyPath}/conf/nginx.conf - -# 创建配置目录 -cd ${openrestyPath} -rm -f openresty-${openrestyVersion}.tar.gz -rm -rf src -mkdir -p /www/wwwroot/default -mkdir -p /www/wwwlogs -mkdir -p /www/server/vhost -mkdir -p /www/server/vhost -mkdir -p /www/server/vhost/rewrite -mkdir -p /www/server/vhost/ssl -mkdir -p /www/server/vhost/acme - -# 写入主配置文件 -cat > ${openrestyPath}/conf/nginx.conf << EOF -# 该文件为OpenResty主配置文件,不建议随意修改! -user www www; -worker_processes auto; -worker_cpu_affinity auto; -worker_rlimit_nofile 65535; -pcre_jit on; -quic_bpf on; -error_log /www/wwwlogs/openresty_error.log crit; -pid /www/server/openresty/nginx.pid; - -stream { - log_format tcp_format '\$time_local|\$remote_addr|\$protocol|\$status|\$bytes_sent|\$bytes_received|\$session_time|\$upstream_addr|\$upstream_bytes_sent|\$upstream_bytes_received|\$upstream_connect_time'; - - access_log /www/wwwlogs/tcp-access.log tcp_format; - error_log /www/wwwlogs/tcp-error.log; -} - -events { - use epoll; - worker_connections 65535; - multi_accept on; -} - -http { - include mime.types; - include proxy.conf; - include default.conf; - - default_type application/octet-stream; - keepalive_timeout 60; - - server_names_hash_bucket_size 512; - client_header_buffer_size 32k; - large_client_header_buffers 4 32k; - client_max_body_size 200m; - client_body_buffer_size 10M; - client_body_in_file_only off; - - variables_hash_max_size 2048; - variables_hash_bucket_size 128; - - http2 on; - http3 on; - quic_gso on; - aio threads; - aio_write on; - directio 512k; - sendfile on; - tcp_nopush on; - tcp_nodelay on; - - fastcgi_connect_timeout 300; - fastcgi_send_timeout 300; - fastcgi_read_timeout 300; - fastcgi_buffer_size 64k; - fastcgi_buffers 8 64k; - fastcgi_busy_buffers_size 256k; - fastcgi_temp_file_write_size 256k; - fastcgi_intercept_errors on; - - gzip on; - gzip_min_length 1k; - gzip_buffers 32 4k; - gzip_http_version 1.1; - gzip_comp_level 6; - gzip_types *; - gzip_vary on; - gzip_proxied any; - gzip_disable "MSIE [1-6]\."; - brotli on; - brotli_comp_level 6; - brotli_min_length 10; - brotli_window 1m; - brotli_types *; - brotli_static on; - - limit_conn_zone \$binary_remote_addr zone=perip:10m; - limit_conn_zone \$server_name zone=perserver:10m; - - server_tokens off; - access_log off; - - waf_http_status general=403 cc_deny=444; - - # 服务状态页 - server { - listen 80; - server_name 127.0.0.1; - allow 127.0.0.1; - - location /nginx_status { - stub_status on; - access_log off; - } - location ~ ^/phpfpm_status/(?\d+)$ { - fastcgi_pass unix:/tmp/php-cgi-\$version.sock; - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME \$fastcgi_script_name; - } - } - include /www/server/vhost/*.conf; -} -EOF -# 写入pathinfo配置文件 -cat > ${openrestyPath}/conf/pathinfo.conf << EOF -set \$real_script_name \$fastcgi_script_name; -if (\$fastcgi_script_name ~ "^(.+?\.php)(/.+)$") { - set \$real_script_name \$1; - set \$path_info \$2; - } -fastcgi_param SCRIPT_FILENAME \$document_root\$real_script_name; -fastcgi_param SCRIPT_NAME \$real_script_name; -fastcgi_param PATH_INFO \$path_info; -EOF -# 写入默认站点页 -cat > ${openrestyPath}/html/index.html << EOF - - - - - - 未找到网站 - 耗子面板 - - - -
-

耗子面板

-

这是耗子面板的 OpenResty 默认页面!

-

当您看到此页面,说明无法在服务器上找到该域名对应的站点。

-

耗子面板 强力驱动

-
- - -EOF - -# 写入站点停止页 -cat > ${openrestyPath}/html/stop.html << EOF - - - - - - 网站已停止 - 耗子面板 - - - -
-

耗子面板

-

该网站已被管理员停止访问!

-

当您看到此页面,说明该网站已被服务器管理员停止对外访问。

-

耗子面板 强力驱动

-
- - -EOF - -# 写入 WAF 拦截页(战未来,暂时无法生效) -cat > ${openrestyPath}/html/block.html << EOF - - - - - - 请求被拦截 - 耗子面板 - - - -
-

耗子面板

-

本次请求判断为危险的攻击请求,已被拦截!

-

可能您的请求中包含了危险的攻击内容,或者您的请求被误判为攻击请求。

-

如果您认为这是误判,请联系服务器管理员解决。

-

耗子面板 强力驱动

-
- - -EOF - -# 处理文件权限 -chmod -R 755 ${openrestyPath} -chmod -R 755 /www/wwwroot -chown -R www:www /www/wwwroot -chmod -R 644 /www/server/vhost - -# 写入无php配置文件 -echo "" > ${openrestyPath}/conf/enable-php-0.conf - -# 自动为所有PHP版本创建配置文件 -if [ -d "${setupPath}/server/php" ]; then - cd ${setupPath}/server/php - phpList=$(ls -l | grep ^d | awk '{print $NF}') - for phpVersion in ${phpList}; do - if [ -d "${setupPath}/server/php/${phpVersion}" ]; then - # 写入PHP配置文件 - cat > ${openrestyPath}/conf/enable-php-${phpVersion}.conf << EOF -location ~ \.php$ { - try_files \$uri =404; - fastcgi_pass unix:/tmp/php-cgi-${phpVersion}.sock; - fastcgi_index index.php; - include fastcgi.conf; - include pathinfo.conf; -} -EOF - fi - done -fi - -# 写入代理默认配置文件 -cat > ${openrestyPath}/conf/proxy.conf << EOF -proxy_temp_path ${openrestyPath}/proxy_temp_dir; -proxy_cache_path ${openrestyPath}/proxy_cache_dir levels=1:2 keys_zone=cache_one:20m inactive=1d max_size=5g; -proxy_connect_timeout 60; -proxy_read_timeout 60; -proxy_send_timeout 60; -proxy_buffer_size 32k; -proxy_buffers 4 64k; -proxy_busy_buffers_size 128k; -proxy_temp_file_write_size 128k; -proxy_next_upstream error timeout invalid_header http_500 http_503 http_404; -proxy_cache cache_one; -EOF - -# 写入默认站点配置文件 -cat > ${openrestyPath}/conf/default.conf << EOF -server -{ - listen 80 default_server reuseport; - listen [::]:80 default_server reuseport; - listen 443 ssl default_server reuseport; - listen [::]:443 ssl default_server reuseport; - server_name _; - index index.html; - root /www/server/openresty/html; - ssl_reject_handshake on; -} -EOF - -# 建立日志目录 -mkdir -p /www/wwwlogs/waf -chown www:www /www/wwwlogs/waf -chmod 755 /www/wwwlogs/waf - -# 写入服务文件 -cat > /etc/systemd/system/openresty.service << EOF -[Unit] -Description=The OpenResty Application Platform -After=syslog.target network-online.target remote-fs.target nss-lookup.target -Wants=network-online.target - -[Service] -Type=forking -PIDFile=/www/server/openresty/nginx.pid -ExecStartPre=/www/server/openresty/sbin/nginx -t -c /www/server/openresty/conf/nginx.conf -ExecStart=/www/server/openresty/sbin/nginx -c /www/server/openresty/conf/nginx.conf -ExecReload=/www/server/openresty/sbin/nginx -s reload -ExecStop=/www/server/openresty/sbin/nginx -s quit -LimitNOFILE=500000 - -[Install] -WantedBy=multi-user.target -EOF - -systemctl daemon-reload -systemctl enable openresty.service -systemctl restart openresty.service - -panel writePlugin openresty ${openrestyVersion} - -echo -e "${HR}\nOpenResty 安装完成\n${HR}" diff --git a/scripts/openresty/uninstall.sh b/scripts/openresty/uninstall.sh deleted file mode 100644 index bb675446..00000000 --- a/scripts/openresty/uninstall.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -systemctl stop openresty -systemctl disable openresty -rm -rf /etc/systemd/system/openresty.service -systemctl daemon-reload -pkill -9 nginx -rm -rf /www/server/openresty - -panel deletePlugin openresty - -echo -e "${HR}\nOpenResty 卸载完成\n${HR}" diff --git a/scripts/panel.service b/scripts/panel.service deleted file mode 100644 index e3434fdf..00000000 --- a/scripts/panel.service +++ /dev/null @@ -1,20 +0,0 @@ -[Unit] -Description=HaoZi Panel -After=syslog.target network.target -Wants=network.target - -[Service] -Type=simple -WorkingDirectory=/www/panel/ -ExecStart=/www/panel/panel --env="/www/panel/panel.conf" -ExecReload=kill -s HUP $MAINPID -ExecStop=kill -s QUIT $MAINPID -User=root -Restart=always -RestartSec=5 -LimitNOFILE=1048576 -LimitNPROC=1048576 -Delegate=yes - -[Install] -WantedBy=multi-user.target diff --git a/scripts/php/install.sh b/scripts/php/install.sh deleted file mode 100644 index 6a1ac75d..00000000 --- a/scripts/php/install.sh +++ /dev/null @@ -1,280 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/php" -setupPath="/www" -phpVersion="${1}" -phpVersionCode="" -phpPath="${setupPath}/server/php/${phpVersion}" -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf install dnf-plugins-core -y - dnf install epel-release -y - dnf config-manager --set-enabled epel - dnf config-manager --set-enabled PowerTools - dnf config-manager --set-enabled powertools - dnf config-manager --set-enabled CRB - dnf config-manager --set-enabled Crb - dnf config-manager --set-enabled crb - /usr/bin/crb enable - dnf makecache - dnf groupinstall "Development Tools" -y - dnf install autoconf glibc-headers gdbm-devel gd gd-devel perl oniguruma-devel libsodium-devel libxml2-devel sqlite-devel libzip-devel bzip2-devel xz-devel libpng-devel libjpeg-devel libwebp-devel libavif-devel freetype-devel gmp-devel openssl-devel readline-devel libxslt-devel libcurl-devel pkgconfig libedit-devel zlib-devel pcre-devel crontabs libicu libicu-devel c-ares -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential autoconf libc6-dev libgdbm-dev libgd-tools libgd-dev perl libonig-dev libsodium-dev libxml2-dev libsqlite3-dev libzip-dev libbz2-dev liblzma-dev libpng-dev libjpeg-dev libwebp-dev libavif-dev libfreetype6-dev libgmp-dev libssl-dev libreadline-dev libxslt1-dev libcurl4-openssl-dev pkg-config libedit-dev zlib1g-dev libpcre3-dev cron libicu-dev libc-ares2 libc-ares-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 准备安装目录 -rm -rf ${phpPath} -mkdir -p ${phpPath} -cd ${phpPath} - -# 下载源码 -if [ "${phpVersion}" == "74" ]; then - phpVersionCode="7.4.33" -elif [ "${phpVersion}" == "80" ]; then - phpVersionCode="8.0.30" -elif [ "${phpVersion}" == "81" ]; then - phpVersionCode="8.1.29" -elif [ "${phpVersion}" == "82" ]; then - phpVersionCode="8.2.20" -elif [ "${phpVersion}" == "83" ]; then - phpVersionCode="8.3.8" -else - echo -e $HR - echo "错误:PHP-${phpVersion}不支持,请检查版本号是否正确。" - exit 1 -fi - -wget -T 120 -t 3 -O ${phpPath}/php-${phpVersionCode}.7z ${downloadUrl}/php-${phpVersionCode}.7z -wget -T 20 -t 3 -O ${phpPath}/php-${phpVersionCode}.7z.checksum.txt ${downloadUrl}/php-${phpVersionCode}.7z.checksum.txt - -if ! sha256sum --status -c php-${phpVersionCode}.7z.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion}源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${phpPath} - exit 1 -fi - -7z x php-${phpVersionCode}.7z -rm -f php-${phpVersionCode}.7z -rm -f php-${phpVersionCode}.7z.checksum.txt -mv php-* src -chmod -R 755 src - -if [ "${phpVersion}" -le "80" ]; then - wget -T 120 -t 3 -O ${phpPath}/openssl-1.1.1w.tar.gz ${downloadUrl}/openssl/openssl-1.1.1w.tar.gz - wget -T 20 -t 3 -O ${phpPath}/openssl-1.1.1w.tar.gz.checksum.txt ${downloadUrl}/openssl/openssl-1.1.1w.tar.gz.checksum.txt - - if ! sha256sum --status -c openssl-1.1.1w.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} OpenSSL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${phpPath} - exit 1 - fi - - tar -zxvf openssl-1.1.1w.tar.gz - rm -f openssl-1.1.1w.tar.gz - rm -f openssl-1.1.1w.tar.gz.checksum.txt - mv openssl-1.1.1w openssl - cd openssl - ./config --prefix=/usr/local/openssl-1.1 --openssldir=/usr/local/openssl-1.1 no-tests - make "-j${j}" - make install - echo "/usr/local/openssl-1.1/lib" > /etc/ld.so.conf.d/openssl-1.1.conf - ldconfig - cd .. - rm -rf openssl - - export CFLAGS="-I/usr/local/openssl-1.1/include -I/usr/local/curl/include" - export LIBS="-L/usr/local/openssl-1.1/lib -L/usr/local/curl/lib" -fi - -# 配置 -cd src -if [ "${phpVersion}" == "81" ] || [ "${phpVersion}" == "82" ] || [ "${phpVersion}" == "83" ]; then - ./configure --prefix=${phpPath} --with-config-file-path=${phpPath}/etc --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-freetype --with-jpeg --with-zlib --enable-xml --disable-rpath --enable-bcmath --enable-shmop --with-curl --enable-mbregex --enable-mbstring --enable-pcntl --enable-ftp --enable-gd --with-openssl --with-mhash --enable-pcntl --enable-sockets --enable-soap --disable-fileinfo --enable-opcache --with-sodium --with-webp --with-avif -else - ./configure --prefix=${phpPath} --with-config-file-path=${phpPath}/etc --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-freetype --with-jpeg --with-zlib --with-libxml-dir=/usr --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-inline-optimization --with-curl --enable-mbregex --enable-mbstring --enable-pcntl --enable-ftp --enable-gd --with-openssl --with-mhash --enable-pcntl --enable-sockets --with-xmlrpc --enable-soap --disable-fileinfo --enable-opcache --with-sodium --with-webp -fi - -# 编译安装 -make "-j${j}" -make install -if [ ! -f "${phpPath}/bin/php" ]; then - echo -e $HR - echo "错误:PHP-${phpVersion}安装失败,请截图错误信息寻求帮助!" - rm -rf ${phpPath} - exit 1 -fi - -# 创建php配置 -mkdir -p ${phpPath}/etc -\cp php.ini-production ${phpPath}/etc/php.ini - -# 安装zip拓展 -cd ${phpPath}/src/ext/zip -${phpPath}/bin/phpize -./configure --with-php-config=${phpPath}/bin/php-config -make "-j${j}" -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PHP-${phpVersion} zip拓展安装失败,请截图错误信息寻求帮助。" - exit 1 -fi -cd ../../ - -# 写入拓展标记位 -echo ";下方标记位禁止删除,否则将导致PHP拓展无法正常安装!" >> ${phpPath}/etc/php.ini -echo ";haozi" >> ${phpPath}/etc/php.ini -# 写入zip拓展到php配置 -echo "extension=zip" >> ${phpPath}/etc/php.ini - -# 设置软链接 -rm -f /usr/bin/php-${phpVersion} -rm -f /usr/bin/pear -rm -f /usr/bin/pecl -ln -sf ${phpPath}/bin/php /usr/bin/php -ln -sf ${phpPath}/bin/php /usr/bin/php-${phpVersion} -ln -sf ${phpPath}/bin/phpize /usr/bin/phpize -ln -sf ${phpPath}/bin/pear /usr/bin/pear -ln -sf ${phpPath}/bin/pecl /usr/bin/pecl -ln -sf ${phpPath}/sbin/php-fpm /usr/bin/php-fpm-${phpVersion} - -# 设置fpm -cat > ${phpPath}/etc/php-fpm.conf << EOF -[global] -pid = ${phpPath}/var/run/php-fpm.pid -error_log = ${phpPath}/var/log/php-fpm.log -log_level = notice - -[www] -listen = /tmp/php-cgi-${phpVersion}.sock -listen.backlog = -1 -listen.allowed_clients = 127.0.0.1 -listen.owner = www -listen.group = www -listen.mode = 0666 -user = www -group = www -pm = dynamic -pm.max_children = 30 -pm.start_servers = 5 -pm.min_spare_servers = 5 -pm.max_spare_servers = 10 -request_terminate_timeout = 100 -request_slowlog_timeout = 30 -pm.status_path = /phpfpm_status/${phpVersion} -slowlog = var/log/slow.log -EOF - -# 设置PHP进程数 -memTotal=$(free -m | grep Mem | awk '{print $2}') -if [[ ${memTotal} -gt 1024 && ${memTotal} -le 2048 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 50#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 5#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 5#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 10#" ${phpPath}/etc/php-fpm.conf -elif [[ ${memTotal} -gt 2048 && ${memTotal} -le 4096 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 80#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 5#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 5#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 20#" ${phpPath}/etc/php-fpm.conf -elif [[ ${memTotal} -gt 4096 && ${memTotal} -le 8192 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 150#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 10#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 10#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 30#" ${phpPath}/etc/php-fpm.conf -elif [[ ${memTotal} -gt 8192 && ${memTotal} -le 16384 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 200#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 15#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 15#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 30#" ${phpPath}/etc/php-fpm.conf -elif [[ ${memTotal} -gt 16384 ]]; then - sed -i "s#pm.max_children.*#pm.max_children = 300#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.start_servers.*#pm.start_servers = 20#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.min_spare_servers.*#pm.min_spare_servers = 20#" ${phpPath}/etc/php-fpm.conf - sed -i "s#pm.max_spare_servers.*#pm.max_spare_servers = 50#" ${phpPath}/etc/php-fpm.conf -fi -sed -i "s#listen.backlog.*#listen.backlog = 8192#" ${phpPath}/etc/php-fpm.conf -# 最大上传限制100M -sed -i 's/post_max_size =.*/post_max_size = 100M/g' ${phpPath}/etc/php.ini -sed -i 's/upload_max_filesize =.*/upload_max_filesize = 100M/g' ${phpPath}/etc/php.ini -# 时区PRC -sed -i 's/;date.timezone =.*/date.timezone = PRC/g' ${phpPath}/etc/php.ini -sed -i 's/short_open_tag =.*/short_open_tag = On/g' ${phpPath}/etc/php.ini -sed -i 's/;cgi.fix_pathinfo=.*/cgi.fix_pathinfo=1/g' ${phpPath}/etc/php.ini -# 最大运行时间 -sed -i 's/max_execution_time =.*/max_execution_time = 86400/g' ${phpPath}/etc/php.ini -sed -i 's/;sendmail_path =.*/sendmail_path = \/usr\/sbin\/sendmail -t -i/g' ${phpPath}/etc/php.ini -# 禁用函数 -sed -i 's/disable_functions =.*/disable_functions = passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv/g' ${phpPath}/etc/php.ini -sed -i 's/display_errors = Off/display_errors = On/g' ${phpPath}/etc/php.ini -sed -i 's/error_reporting =.*/error_reporting = E_ALL \& \~E_NOTICE/g' ${phpPath}/etc/php.ini - -# 设置SSL根证书 -#sed -i "s#;openssl.cafile=#openssl.cafile=/etc/pki/tls/certs/ca-bundle.crt#" ${phpPath}/etc/php.ini -#sed -i "s#;curl.cainfo =#curl.cainfo = /etc/pki/tls/certs/ca-bundle.crt#" ${phpPath}/etc/php.ini - -# 关闭php外显 -sed -i 's/expose_php = On/expose_php = Off/g' ${phpPath}/etc/php.ini - -# 写入openresty 调用php配置文件 -cat > /www/server/openresty/conf/enable-php-${phpVersion}.conf << EOF -location ~ \.php$ { - try_files \$uri =404; - fastcgi_pass unix:/tmp/php-cgi-${phpVersion}.sock; - fastcgi_index index.php; - include fastcgi.conf; - include pathinfo.conf; -} -EOF - -# 添加php-fpm到服务 -\cp ${phpPath}/src/sapi/fpm/php-fpm.service /lib/systemd/system/php-fpm-${phpVersion}.service -sed -i "/PrivateTmp/d" /lib/systemd/system/php-fpm-${phpVersion}.service -systemctl daemon-reload - -# 启动php -systemctl enable php-fpm-${phpVersion}.service -systemctl start php-fpm-${phpVersion}.service - -panel writePlugin php${phpVersion} ${phpVersionCode} - -echo -e "${HR}\nPHP-${phpVersion} 安装完成\n${HR}" diff --git a/scripts/php/uninstall.sh b/scripts/php/uninstall.sh deleted file mode 100644 index a2f0947c..00000000 --- a/scripts/php/uninstall.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -setupPath="/www" -phpVersion="${1}" -phpPath="${setupPath}/server/php/${phpVersion}" - -systemctl stop php-fpm-${phpVersion} -systemctl disable php-fpm-${phpVersion} -rm -rf /lib/systemd/system/php-fpm-${phpVersion}.service -systemctl daemon-reload - -# 检查是否存在phpMyAdmin -if [ -d "${setupPath}/server/phpmyadmin" ]; then - sed -i "s/enable-php-${phpVersion}/enable-php-0/g" ${setupPath}/server/vhost/phpmyadmin.conf - systemctl reload openresty -fi - -rm -rf ${phpPath} -rm -f /usr/bin/php-${phpVersion} - -panel deletePlugin php${phpVersion} - -echo -e "${HR}\nPHP-${phpVersion} 卸载完成\n${HR}" diff --git a/scripts/php_extensions/Swow.sh b/scripts/php_extensions/Swow.sh deleted file mode 100644 index 3894e3d0..00000000 --- a/scripts/php_extensions/Swow.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -swowVersion="1.4.1" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=swow') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 swow" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf swow - rm -rf swow-${swowVersion}.zip - wget -T 60 -t 3 -O swow-${swowVersion}.zip ${downloadUrl}/swow-${swowVersion}.zip - wget -T 20 -t 3 -O swow-${swowVersion}.zip.checksum.txt ${downloadUrl}/swow-${swowVersion}.zip.checksum.txt - - if ! sha256sum --status -c swow-${swowVersion}.zip.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} swow 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - unzip swow-${swowVersion}.zip - mv swow-${swowVersion} swow - rm -f swow-${swowVersion}.zip - rm -f swow-${swowVersion}.zip.checksum.txt - cd swow/ext - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} swow 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} swow 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=swow' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} swow 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=swow$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 swow" - exit 1 - fi - - sed -i '/extension=swow/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} swow 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/Zend OPcache.sh b/scripts/php_extensions/Zend OPcache.sh deleted file mode 100644 index d106d814..00000000 --- a/scripts/php_extensions/Zend OPcache.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -action="$1" # 操作 -phpVersion="$2" # PHP版本 - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^zend_extension=opcache$') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 Zend OPcache" - exit 1 - fi - - if [ "${phpVersion}" -ge "80" ]; then - sed -i '/;haozi/a\zend_extension=opcache\nopcache.enable = 1\nopcache.enable_cli=1\nopcache.memory_consumption=128\nopcache.interned_strings_buffer=32\nopcache.max_accelerated_files=100000\nopcache.revalidate_freq=3\nopcache.save_comments=0\nopcache.jit_buffer_size=128m\nopcache.jit=1205' /www/server/php/${phpVersion}/etc/php.ini - else - sed -i '/;haozi/a\zend_extension=opcache\nopcache.enable = 1\nopcache.enable_cli=1\nopcache.memory_consumption=128\nopcache.interned_strings_buffer=32\nopcache.max_accelerated_files=100000\nopcache.revalidate_freq=3\nopcache.save_comments=0' /www/server/php/${phpVersion}/etc/php.ini - fi - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} Zend OPcache 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^zend_extension=opcache$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 Zend OPcache" - exit 1 - fi - - sed -i '/^opcache.*$/d' /www/server/php/${phpVersion}/etc/php.ini - sed -i '/zend_extension=opcache/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} Zend OPcache 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/igbinary.sh b/scripts/php_extensions/igbinary.sh deleted file mode 100644 index 5216f369..00000000 --- a/scripts/php_extensions/igbinary.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -igbinaryVersion="3.2.15" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=igbinary') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 igbinary" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf igbinary - rm -rf igbinary-${igbinaryVersion}.zip - wget -T 60 -t 3 -O igbinary-${igbinaryVersion}.zip ${downloadUrl}/igbinary-${igbinaryVersion}.zip - wget -T 20 -t 3 -O igbinary-${igbinaryVersion}.zip.checksum.txt ${downloadUrl}/igbinary-${igbinaryVersion}.zip.checksum.txt - - if ! sha256sum --status -c igbinary-${igbinaryVersion}.zip.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} igbinary 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - unzip igbinary-${igbinaryVersion}.zip - mv igbinary-${igbinaryVersion} igbinary - rm -f igbinary-${igbinaryVersion}.zip - rm -f igbinary-${igbinaryVersion}.zip.checksum.txt - cd igbinary - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} igbinary 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} igbinary 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=igbinary' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} igbinary 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=igbinary$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 igbinary" - exit 1 - fi - - sed -i '/extension=igbinary/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} igbinary 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/imagick.sh b/scripts/php_extensions/imagick.sh deleted file mode 100644 index 3efb39e0..00000000 --- a/scripts/php_extensions/imagick.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -imagickVersion="3.7.0" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=imagick$') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 imagick" - exit 1 - fi - - # 安装依赖 - if [ "${OS}" == "centos" ]; then - dnf install ImageMagick ImageMagick-devel -y - elif [ "${OS}" == "debian" ]; then - apt-get install imagemagick libmagickwand-dev -y - else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf imagick - rm -rf imagick-${imagickVersion}.tar.gz - wget -T 60 -t 3 -O imagick-${imagickVersion}.tar.gz ${downloadUrl}/imagick-${imagickVersion}.tar.gz - wget -T 20 -t 3 -O imagick-${imagickVersion}.tar.gz.checksum.txt ${downloadUrl}/imagick-${imagickVersion}.tar.gz.checksum.txt - - if ! sha256sum --status -c imagick-${imagickVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} imagick 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - tar -zxvf imagick-${imagickVersion}.tar.gz - rm -f imagick-${imagickVersion}.tar.gz - rm -f imagick-${imagickVersion}.tar.gz.checksum.txt - mv imagick-${imagickVersion} imagick - cd imagick - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} imagick 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} imagick 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=imagick' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} imagick 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=imagick$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 imagick" - exit 1 - fi - - sed -i '/extension=imagick/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} imagick 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/ionCube Loader.sh b/scripts/php_extensions/ionCube Loader.sh deleted file mode 100644 index 33d2947b..00000000 --- a/scripts/php_extensions/ionCube Loader.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep 'ioncube_loader_lin') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 ionCube" - exit 1 - fi - - mkdir /usr/local/ioncube - cd /usr/local/ioncube - wget -T 60 -t 3 -O /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so ${downloadUrl}/ioncube_loader_lin_${phpVersion}.so - wget -T 20 -t 3 -O /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so.checksum.txt ${downloadUrl}/ioncube_loader_lin_${phpVersion}.so.checksum.txt - - if ! sha256sum --status -c /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} ionCube 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - rm -f /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so.checksum.txt - - sed -i -e "/;haozi/a\zend_extension=/usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so" /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} ionCube 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep 'ioncube_loader_lin') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 ionCube" - exit 1 - fi - - rm -f /usr/local/ioncube/ioncube_loader_lin_${phpVersion}.so - sed -i '/ioncube_loader_lin/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} ionCube 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/official.sh b/scripts/php_extensions/official.sh deleted file mode 100644 index 3d624e6a..00000000 --- a/scripts/php_extensions/official.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -action="$1" # 操作 -phpVersion="$2" # PHP版本 -extensionName="$3" # 扩展名称 -addArgs="" # 附加参数 - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep "^extension=${extensionName}$") - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 ${extensionName}" - exit 1 - fi - - # 安装依赖 - if [ "${extensionName}" == "snmp" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y net-snmp-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libsnmp-dev - fi - fi - if [ "${extensionName}" == "ldap" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y openldap-devel - ln -sf /usr/lib64/libldap* /usr/lib - elif [ "${OS}" == "debian" ]; then - apt-get install -y libldap2-dev - ln -sf /usr/lib/x86_64-linux-gnu/libldap* /usr/lib - fi - fi - if [ "${extensionName}" == "imap" ]; then - if [ "${OS}" == "centos" ]; then - # RHEL 9 的仓库中没有 libc-client-devel,待考虑 - dnf install -y libc-client-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libc-client-dev - fi - addArgs="--with-imap --with-imap-ssl --with-kerberos" - fi - if [ "${extensionName}" == "enchant" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y enchant-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libenchant-2-dev - fi - fi - if [ "${extensionName}" == "pspell" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y aspell-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libpspell-dev - fi - fi - if [ "${extensionName}" == "gmp" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y gmp-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libgmp-dev - fi - fi - if [ "${extensionName}" == "gettext" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y gettext-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libgettextpo-dev - fi - fi - if [ "${extensionName}" == "bz2" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y bzip2-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libbz2-dev - fi - fi - if [ "${extensionName}" == "zip" ]; then - if [ "${OS}" == "centos" ]; then - dnf install -y libzip-devel - elif [ "${OS}" == "debian" ]; then - apt-get install -y libzip-dev - fi - fi - if [ "${extensionName}" == "pdo_pgsql" ]; then - addArgs="--with-pdo-pgsql=/www/server/postgresql" - fi - - # 安装扩展 - if [ ! -d /www/server/php/${phpVersion}/src/ext/${extensionName} ]; then - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 源码不存在" - exit 1 - fi - cd /www/server/php/${phpVersion}/src/ext/${extensionName} - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config ${addArgs} - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 安装失败" - exit 1 - fi - - sed -i "/;haozi/a\extension=${extensionName}" /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep "^extension=${extensionName}$") - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 ${extensionName}" - exit 1 - fi - - sed -i "/extension=${extensionName}/d" /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} ${extensionName} 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/redis.sh b/scripts/php_extensions/redis.sh deleted file mode 100644 index 442c4405..00000000 --- a/scripts/php_extensions/redis.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -phpredisVersion="5.3.7" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=redis$') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 redis" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf phpredis - rm -rf phpredis-${phpredisVersion}.tar.gz - wget -T 60 -t 3 -O phpredis-${phpredisVersion}.tar.gz ${downloadUrl}/phpredis-${phpredisVersion}.tar.gz - wget -T 20 -t 3 -O phpredis-${phpredisVersion}.tar.gz.checksum.txt ${downloadUrl}/phpredis-${phpredisVersion}.tar.gz.checksum.txt - - if ! sha256sum --status -c phpredis-${phpredisVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} redis 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - tar -zxvf phpredis-${phpredisVersion}.tar.gz - mv phpredis-${phpredisVersion} phpredis - rm -f phpredis-${phpredisVersion}.tar.gz - rm -f phpredis-${phpredisVersion}.tar.gz.checksum.txt - cd phpredis - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} redis 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} redis 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=redis' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} redis 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=redis$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 redis" - exit 1 - fi - - sed -i '/extension=redis/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} redis 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/php_extensions/swoole.sh b/scripts/php_extensions/swoole.sh deleted file mode 100644 index e6f4101c..00000000 --- a/scripts/php_extensions/swoole.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions" -action="$1" -phpVersion="$2" -swooleVersion="5.1.2" - -Install() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=swoole') - if [ "${isInstall}" != "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 已安装 swoole" - exit 1 - fi - - cd /www/server/php/${phpVersion}/src/ext - rm -rf swoole - rm -rf swoole-src-${swooleVersion}.zip - wget -T 60 -t 3 -O swoole-src-${swooleVersion}.zip ${downloadUrl}/swoole-src-${swooleVersion}.zip - wget -T 20 -t 3 -O swoole-src-${swooleVersion}.zip.checksum.txt ${downloadUrl}/swoole-src-${swooleVersion}.zip.checksum.txt - - if ! sha256sum --status -c swoole-src-${swooleVersion}.zip.checksum.txt; then - echo -e $HR - echo "错误:PHP-${phpVersion} swoole 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 - fi - - unzip swoole-src-${swooleVersion}.zip - mv swoole-src-${swooleVersion} swoole - rm -f swoole-src-${swooleVersion}.zip - rm -f swoole-src-${swooleVersion}.zip.checksum.txt - cd swoole - /www/server/php/${phpVersion}/bin/phpize - ./configure --with-php-config=/www/server/php/${phpVersion}/bin/php-config - make - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} swoole 编译失败" - exit 1 - fi - make install - if [ "$?" != "0" ]; then - echo -e $HR - echo "PHP-${phpVersion} swoole 安装失败" - exit 1 - fi - - sed -i '/;haozi/a\extension=swoole' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} swoole 安装成功" -} - -Uninstall() { - # 检查是否已经安装 - isInstall=$(cat /www/server/php/${phpVersion}/etc/php.ini | grep '^extension=swoole$') - if [ "${isInstall}" == "" ]; then - echo -e $HR - echo "PHP-${phpVersion} 未安装 swoole" - exit 1 - fi - - sed -i '/extension=swoole/d' /www/server/php/${phpVersion}/etc/php.ini - - # 重载PHP - systemctl reload php-fpm-${phpVersion}.service - echo -e $HR - echo "PHP-${phpVersion} swoole 卸载成功" -} - -if [ "$action" == 'install' ]; then - Install -fi -if [ "$action" == 'uninstall' ]; then - Uninstall -fi diff --git a/scripts/phpmyadmin/install.sh b/scripts/phpmyadmin/install.sh deleted file mode 100644 index 34531868..00000000 --- a/scripts/phpmyadmin/install.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/phpmyadmin" -setupPath="/www" -phpmyadminPath="${setupPath}/server/phpmyadmin" -phpmyadminVersion="5.2.1" -randomDir="$(cat /dev/urandom | head -n 16 | md5sum | head -c 10)" - -# 准备安装目录 -rm -rf ${phpmyadminPath} -mkdir -p ${phpmyadminPath} -cd ${phpmyadminPath} - -wget -T 60 -t 3 -O phpMyAdmin-${phpmyadminVersion}-all-languages.zip ${downloadUrl}/phpMyAdmin-${phpmyadminVersion}-all-languages.zip -wget -T 20 -t 3 -O phpMyAdmin-${phpmyadminVersion}-all-languages.zip.checksum.txt ${downloadUrl}/phpMyAdmin-${phpmyadminVersion}-all-languages.zip.checksum.txt - -if ! sha256sum --status -c phpMyAdmin-${phpmyadminVersion}-all-languages.zip.checksum.txt; then - echo -e $HR - echo "错误:phpMyAdmin 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${phpmyadminPath} - exit 1 -fi - -unzip -o phpMyAdmin-${phpmyadminVersion}-all-languages.zip -mv phpMyAdmin-${phpmyadminVersion}-all-languages phpmyadmin_${randomDir} -chown -R www:www ${phpmyadminPath} -chmod -R 755 ${phpmyadminPath} -rm -rf phpMyAdmin-${phpmyadminVersion}-all-languages.zip -rm -rf phpMyAdmin-${phpmyadminVersion}-all-languages.zip.checksum.txt - -# 判断PHP版本 -phpVersion="" -if [ -d "/www/server/php/74" ]; then - phpVersion="74" -fi -if [ -d "/www/server/php/80" ]; then - phpVersion="80" -fi -if [ -d "/www/server/php/81" ]; then - phpVersion="81" -fi -if [ -d "/www/server/php/82" ]; then - phpVersion="82" -fi - -if [ "${phpVersion}" == "" ]; then - echo -e $HR - echo "错误:未安装 PHP" - rm -rf ${phpmyadminPath} - exit 1 -fi - -# 写入 phpMyAdmin 配置文件 -cat > /www/server/vhost/phpmyadmin.conf << EOF -# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! -# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 -server -{ - # port标记位开始 - listen 888; - # port标记位结束 - # server_name标记位开始 - server_name phpmyadmin; - # server_name标记位结束 - # index标记位开始 - index index.php; - # index标记位结束 - # root标记位开始 - root /www/server/phpmyadmin; - # root标记位结束 - - # php标记位开始 - include enable-php-${phpVersion}.conf; - # php标记位结束 - - # 面板默认禁止访问部分敏感目录,可自行修改 - location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) - { - return 404; - } - location ~ /tmp/ { - return 403; - } - # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 - location ~ .*\.(js|css)$ - { - expires 1h; - error_log /dev/null; - access_log /dev/null; - } - - access_log /www/wwwlogs/phpmyadmin.log; - error_log /www/wwwlogs/phpmyadmin.log; -} -EOF -# 设置文件权限 -chown -R root:root /www/server/vhost/phpmyadmin.conf -chmod -R 644 /www/server/vhost/phpmyadmin.conf -chmod -R 755 ${phpmyadminPath} -chown -R www:www ${phpmyadminPath} - -# 放行端口 -if [ "${OS}" == "centos" ]; then - firewall-cmd --permanent --zone=public --add-port=888/tcp > /dev/null 2>&1 - firewall-cmd --reload -elif [ "${OS}" == "debian" ]; then - ufw allow 888/tcp > /dev/null 2>&1 - ufw reload -fi - -panel writePlugin phpmyadmin 5.2.1 -systemctl reload openresty - -echo -e "${HR}\phpMyAdmin 安装完成\n${HR}" diff --git a/scripts/phpmyadmin/uninstall.sh b/scripts/phpmyadmin/uninstall.sh deleted file mode 100644 index 5566716d..00000000 --- a/scripts/phpmyadmin/uninstall.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -setupPath="/www" -phpmyadminPath="${setupPath}/server/phpmyadmin" - - -rm -rf ${setupPath}/server/vhost/phpmyadmin.conf -rm -rf ${phpmyadminPath} -panel deletePlugin phpmyadmin -systemctl reload openresty - -echo -e "${HR}\phpMyAdmin 卸载完成\n${HR}" diff --git a/scripts/podman/install.sh b/scripts/podman/install.sh deleted file mode 100644 index 9cf07ad8..00000000 --- a/scripts/podman/install.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -podmanVersion="4.0.0" - -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf install podman -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install podman containers-storage -y - # Debian下不清楚是不是Bug,需要手动复制存储配置到正确位置 - cp /usr/share/containers/storage.conf /etc/containers/storage.conf -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -systemctl enable podman -systemctl enable podman.socket -systemctl enable podman-restart -systemctl start podman -systemctl start podman.socket -systemctl start podman-restart - -panel writePlugin podman ${podmanVersion} -echo -e ${HR} -echo "podman 安装完成" -echo -e ${HR} diff --git a/scripts/podman/uninstall.sh b/scripts/podman/uninstall.sh deleted file mode 100644 index 29d142e3..00000000 --- a/scripts/podman/uninstall.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - - -systemctl stop podman-restart -systemctl stop podman -systemctl disable podman-restart -systemctl disable podman - -if [ "${OS}" == "centos" ]; then - dnf remove podman -y -elif [ "${OS}" == "debian" ]; then - apt-get remove podman -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -panel deletePlugin podman -echo -e $HR -echo "podman 卸载完成" -echo -e $HR diff --git a/scripts/podman/update.sh b/scripts/podman/update.sh deleted file mode 100644 index da0f48bd..00000000 --- a/scripts/podman/update.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/www/server/bin:/www/server/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -podmanVersion="4.0.0" - -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf update podman -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get upgrade podman -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -systemctl restart podman - -panel writePlugin podman ${podmanVersion} -echo -e ${HR} -echo "podman 安装完成" -echo -e ${HR} diff --git a/scripts/postgresql/install.sh b/scripts/postgresql/install.sh deleted file mode 100644 index 39c15080..00000000 --- a/scripts/postgresql/install.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}') -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/postgresql" -setupPath="/www" -postgresqlPath="${setupPath}/server/postgresql" -postgresqlVersion="" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -if [[ "${1}" == "15" ]]; then - postgresqlVersion="15.7" -elif [[ "${1}" == "16" ]]; then - postgresqlVersion="16.3" -else - echo -e $HR - echo "错误:不支持的 PostgreSQL 版本!" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install make gettext zlib-devel readline-devel libicu-devel libxml2-devel libxslt-devel openssl-devel systemd-devel -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential make gettext zlib1g-dev libreadline-dev libicu-dev libxml2-dev libxslt-dev libssl-dev libsystemd-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -postgresqlUserCheck=$(cat /etc/passwd | grep postgres) -if [ "${postgresqlUserCheck}" == "" ]; then - groupadd postgres - useradd -g postgres postgres -fi - -# 准备目录 -rm -rf ${postgresqlPath} -mkdir -p ${postgresqlPath} -cd ${postgresqlPath} - -# 下载源码 -wget -T 120 -t 3 -O ${postgresqlPath}/postgresql-${postgresqlVersion}.7z ${downloadUrl}/postgresql-${postgresqlVersion}.7z -wget -T 20 -t 3 -O ${postgresqlPath}/postgresql-${postgresqlVersion}.7z.checksum.txt ${downloadUrl}/postgresql-${postgresqlVersion}.7z.checksum.txt - -if ! sha256sum --status -c postgresql-${postgresqlVersion}.7z.checksum.txt; then - echo -e $HR - echo "错误:PostgreSQL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${postgresqlPath} - exit 1 -fi - -7z x postgresql-${postgresqlVersion}.7z -rm -f postgresql-${postgresqlVersion}.7z -rm -f postgresql-${postgresqlVersion}.7z.checksum.txt -mv postgresql-${postgresqlVersion} src -chmod -R 755 src - -# 编译 -cd src -./configure --prefix=${postgresqlPath} --enable-nls='zh_CN en' --with-icu --with-ssl=openssl --with-systemd --with-libxml --with-libxslt -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 编译初始化失败,请截图错误信息寻求帮助。" - rm -rf ${postgresqlPath} - exit 1 -fi -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 编译失败,请截图错误信息寻求帮助。" - rm -rf ${postgresqlPath} - exit 1 -fi -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 安装失败,请截图错误信息寻求帮助。" - rm -rf ${postgresqlPath} - exit 1 -fi - -cd ${postgresqlPath} -rm -rf ${postgresqlPath}/src - -# 配置 -mkdir -p ${postgresqlPath}/data -mkdir -p ${postgresqlPath}/logs -chown -R postgres:postgres ${postgresqlPath} -chmod -R 700 ${postgresqlPath} - -echo "export PATH=${postgresqlPath}/bin:\$PATH" >> /etc/profile -source /etc/profile - -mkdir -p /home/postgres -cd /home/postgres -if [ -f /home/postgres/.bash_profile ]; then - echo "export PGHOME=${postgresqlPath}" >> /home/postgres/.bash_profile - echo "export PGDATA=${postgresqlPath}/data" >> /home/postgres/.bash_profile - echo "export PATH=${postgresqlPath}/bin:\$PATH " >> /home/postgres/.bash_profile - echo "MANPATH=$PGHOME/share/man:$MANPATH" >> /home/postgres/.bash_profile - echo "LD_LIBRARY_PATH=$PGHOME/lib:$LD_LIBRARY_PATH" >> /home/postgres/.bash_profile - source /home/postgres/.bash_profile -fi -if [ -f /home/postgres/.profile ]; then - echo "export PGHOME=${postgresqlPath}" >> /home/postgres/.profile - echo "export PGDATA=${postgresqlPath}/data" >> /home/postgres/.profile - echo "export PATH=${postgresqlPath}/bin:\$PATH " >> /home/postgres/.profile - echo "MANPATH=$PGHOME/share/man:$MANPATH" >> /home/postgres/.profile - echo "LD_LIBRARY_PATH=$PGHOME/lib:$LD_LIBRARY_PATH" >> /home/postgres/.profile - source /home/postgres/.profile -fi - -# 初始化 -su - postgres -c "${postgresqlPath}/bin/initdb -D ${postgresqlPath}/data" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 初始化失败,请截图错误信息寻求帮助。" - rm -rf ${postgresqlPath} - exit 1 -fi - -# 配置慢查询日志 -cat >> ${postgresqlPath}/data/postgresql.conf << EOF -logging_collector = on -log_destination = 'stderr' -log_directory = '${postgresqlPath}/logs' -log_filename = 'postgresql-%Y-%m-%d.log' -log_statement = all -log_min_duration_statement = 5000 -EOF - -# 写入服务 -cat > /etc/systemd/system/postgresql.service << EOF -[Unit] -Description=PostgreSQL database server -Documentation=man:postgres(1) -After=network-online.target -Wants=network-online.target - -[Service] -Type=notify -User=postgres -ExecStart=${postgresqlPath}/bin/postgres -D ${postgresqlPath}/data -ExecReload=/bin/kill -HUP \$MAINPID -KillMode=mixed -KillSignal=SIGINT -TimeoutSec=infinity -LimitNOFILE=500000 - -[Install] -WantedBy=multi-user.target -EOF - -# 在 /etc/systemd/logind.conf 设置 RemoveIPC=no,不然会删除 /dev/shm 下的共享内存文件 -checkRemoveIPC=$(cat /etc/systemd/logind.conf | grep '^RemoveIPC=no.*$') -if [ "${checkRemoveIPC}" == "" ]; then - echo "RemoveIPC=no" >> /etc/systemd/logind.conf - systemctl restart systemd-logind -fi - -# 启动服务 -systemctl daemon-reload -systemctl enable postgresql -systemctl start postgresql - -panel writePlugin postgresql${1} ${postgresqlVersion} - -echo -e "${HR}\nPostgreSQL-${1} 安装完成\n${HR}" diff --git a/scripts/postgresql/uninstall.sh b/scripts/postgresql/uninstall.sh deleted file mode 100644 index c9231f42..00000000 --- a/scripts/postgresql/uninstall.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -systemctl stop postgresql -systemctl disable postgresql -rm -rf /etc/systemd/system/postgresql.service -systemctl daemon-reload -pkill -9 postgresql -rm -rf /www/server/postgresql - -userdel -r postgres -groupdel postgres - -sed -i '/export PATH=\/www\/server\/postgresql/d' /etc/profile -source /etc/profile - -panel deletePlugin postgresql${1} - -echo -e "${HR}\nPostgreSQL-${1} 卸载完成\n${HR}" diff --git a/scripts/postgresql/update.sh b/scripts/postgresql/update.sh deleted file mode 100644 index 8f8de29d..00000000 --- a/scripts/postgresql/update.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}') -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/postgresql" -setupPath="/www" -postgresqlPath="${setupPath}/server/postgresql" -postgresqlVersion="" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -if [[ "${1}" == "15" ]]; then - postgresqlVersion="15.7" -elif [[ "${1}" == "16" ]]; then - postgresqlVersion="16.3" -else - echo -e $HR - echo "错误:不支持的 PostgreSQL 版本!" - exit 1 -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install make gettext zlib-devel readline-devel libicu-devel libxml2-devel libxslt-devel openssl-devel systemd-devel -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential make gettext zlib1g-dev libreadline-dev libicu-dev libxml2-dev libxslt-dev libssl-dev libsystemd-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi - -# 停止已有服务 -systemctl stop postgresql - -# 准备目录 -rm -rf ${postgresqlPath}/src -cd ${postgresqlPath} - -# 下载源码 -wget -T 120 -t 3 -O ${postgresqlPath}/postgresql-${postgresqlVersion}.7z ${downloadUrl}/postgresql-${postgresqlVersion}.7z -wget -T 20 -t 3 -O ${postgresqlPath}/postgresql-${postgresqlVersion}.7z.checksum.txt ${downloadUrl}/postgresql-${postgresqlVersion}.7z.checksum.txt - -if ! sha256sum --status -c postgresql-${postgresqlVersion}.7z.checksum.txt; then - echo -e $HR - echo "错误:PostgreSQL 源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - exit 1 -fi - -7z x postgresql-${postgresqlVersion}.7z -rm -f postgresql-${postgresqlVersion}.7z -rm -f postgresql-${postgresqlVersion}.7z.checksum.txt -mv postgresql-${postgresqlVersion} src -chmod -R 755 src - -# 编译 -cd src -./configure --prefix=${postgresqlPath} --enable-nls='zh_CN en' --with-icu --with-ssl=openssl --with-systemd --with-libxml --with-libxslt -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 编译初始化失败,请截图错误信息寻求帮助。" - exit 1 -fi -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 编译失败,请截图错误信息寻求帮助。" - exit 1 -fi -make install -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:PostgreSQL 安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -cd ${postgresqlPath} -rm -rf ${postgresqlPath}/src - -# 配置 -chown -R postgres:postgres ${postgresqlPath} -chmod -R 700 ${postgresqlPath} - -panel writePlugin postgresql${1} ${postgresqlVersion} - -systemctl daemon-reload -systemctl restart postgresql - -echo -e "${HR}\nPostgreSQL-${1} 升级完成\n${HR}" diff --git a/scripts/pureftpd/install.sh b/scripts/pureftpd/install.sh deleted file mode 100644 index b1d633b3..00000000 --- a/scripts/pureftpd/install.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/pure-ftpd" -setupPath="/www" -pureftpdPath="${setupPath}/server/pure-ftpd" -pureftpdVersion="1.0.50" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -# 准备安装目录 -rm -rf ${pureftpdPath} -mkdir -p ${pureftpdPath} -cd ${pureftpdPath} - -wget -T 120 -t 3 -O ${pureftpdPath}/pure-ftpd-${pureftpdVersion}.tar.gz ${downloadUrl}/pure-ftpd-${pureftpdVersion}.tar.gz -wget -T 20 -t 3 -O ${pureftpdPath}/pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt ${downloadUrl}/pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${pureftpdPath} - exit 1 -fi - -tar -xvf pure-ftpd-${pureftpdVersion}.tar.gz -rm -f pure-ftpd-${pureftpdVersion}.tar.gz -rm -f pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt -mv pure-ftpd-${pureftpdVersion} src -cd src - -./configure --prefix=${pureftpdPath} CFLAGS=-O2 --with-puredb --with-quotas --with-cookie --with-virtualhosts --with-diraliases --with-sysquotas --with-ratios --with-altlog --with-paranoidmsg --with-shadow --with-welcomemsg --with-throttling --with-uploadscript --with-language=simplified-chinese --with-rfc2640 --with-ftpwho --with-tls -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}编译配置失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}编译失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make install -if [ ! -f "${pureftpdPath}/bin/pure-pw" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 修改 pure-ftpd 配置文件 -sed -i "s!# PureDB\s*/etc/pureftpd.pdb!PureDB ${pureftpdPath}/etc/pureftpd.pdb!" ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!# ChrootEveryone\s*yes!ChrootEveryone yes!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!NoAnonymous\s*no!NoAnonymous yes!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!AnonymousCanCreateDirs\s*yes!AnonymousCanCreateDirs no!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!AnonymousCantUpload\s*yes!AnonymousCantUpload no!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!PAMAuthentication\s*yes!PAMAuthentication no!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!UnixAuthentication\s*yes!UnixAuthentication no!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!# PassivePortRange\s*30000 50000!PassivePortRange 39000 40000!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!PassivePortRange\s*30000 50000!PassivePortRange 39000 40000!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!LimitRecursion\s*10000 8!LimitRecursion 20000 8!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!# TLS!TLS!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i "s!# CertFile\s*/etc/ssl/private/pure-ftpd.pem!CertFile ${pureftpdPath}/etc/pure-ftpd.pem!" ${pureftpdPath}/etc/pure-ftpd.conf -sed -i 's!# Bind\s*127.0.0.1,21!Bind 0.0.0.0,21!' ${pureftpdPath}/etc/pure-ftpd.conf -sed -i "s!# PIDFile\s*/var/run/pure-ftpd.pid!PIDFile ${pureftpdPath}/etc/pure-ftpd.pid!" ${pureftpdPath}/etc/pure-ftpd.conf -touch ${pureftpdPath}/etc/pureftpd.passwd -touch ${pureftpdPath}/etc/pureftpd.pdb - -openssl dhparam -out ${pureftpdPath}/etc/pure-ftpd-dhparams.pem 2048 -openssl req -x509 -nodes -days 36500 -newkey rsa:2048 -sha256 -keyout ${pureftpdPath}/etc/pure-ftpd.pem -out ${pureftpdPath}/etc/pure-ftpd.pem -subj "/C=CN/ST=Tianjin/L=Tianjin/O=HaoZi Technology Co., Ltd./OU=HaoZi Panel/CN=Panel" -chmod 600 ${pureftpdPath}/etc/*.pem - -# 添加系统服务 -ln -sf ${pureftpdPath}/bin/pure-pw /usr/bin/pure-pw - -cat > /etc/systemd/system/pure-ftpd.service << EOF -[Unit] -Description=Pure-FTPd FTP server -After=syslog.target network.target - -[Service] -Type=forking -PIDFile=${pureftpdPath}/etc/pure-ftpd.pid -ExecStart=${pureftpdPath}/sbin/pure-ftpd ${pureftpdPath}/etc/pure-ftpd.conf -ExecStartPost=/bin/sleep 2 -ExecStop=/bin/kill -TERM \$MAINPID - -[Install] -WantedBy=multi-user.target -EOF - -# 添加防火墙规则 -if [ "${OS}" == "centos" ]; then - firewall-cmd --zone=public --add-port=21/tcp --permanent - firewall-cmd --zone=public --add-port=39000-40000/tcp --permanent - firewall-cmd --reload -elif [ "${OS}" == "debian" ]; then - ufw allow 21/tcp - ufw allow 39000:40000/tcp - ufw reload -fi - -systemctl daemon-reload -systemctl enable pure-ftpd.service -systemctl start pure-ftpd.service - -panel writePlugin pureftpd 1.0.50 - -echo -e "${HR}\nPure-Ftpd-${pureftpdVersion} 安装完成\n${HR}" diff --git a/scripts/pureftpd/uninstall.sh b/scripts/pureftpd/uninstall.sh deleted file mode 100644 index 34067713..00000000 --- a/scripts/pureftpd/uninstall.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -systemctl stop pure-ftpd -systemctl disable pure-ftpd -rm -f /etc/systemd/system/pure-ftpd.service -systemctl daemon-reload -pkill -9 pure-ftpd -rm -rf /www/server/pure-ftpd -rm -f /usr/bin/pure-pw - -panel deletePlugin pureftpd - -echo -e "${HR}\nPure-Ftpd 卸载完成\n${HR}" diff --git a/scripts/pureftpd/update.sh b/scripts/pureftpd/update.sh deleted file mode 100644 index f4fa90c2..00000000 --- a/scripts/pureftpd/update.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/pure-ftpd" -setupPath="/www" -pureftpdPath="${setupPath}/server/pure-ftpd" -pureftpdVersion="1.0.50" - -source ${setupPath}/panel/scripts/calculate_j.sh -j=$(calculate_j) - -# 准备安装目录 -cp ${pureftpdPath}/etc/pureftpd.passwd /tmp/pureftpd.passwd -cp ${pureftpdPath}/etc/pureftpd.pdb /tmp/pureftpd.pdb -cp ${pureftpdPath}/etc/pureftpd.conf /tmp/pureftpd.conf -systemctl stop pure-ftpd.service -rm -rf ${pureftpdPath} -mkdir -p ${pureftpdPath} -cd ${pureftpdPath} - -wget -T 60 -t 3 -O ${pureftpdPath}/pure-ftpd-${pureftpdVersion}.tar.gz ${downloadUrl}/pure-ftpd-${pureftpdVersion}.tar.gz -wget -T 20 -t 3 -O ${pureftpdPath}/pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt ${downloadUrl}/pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${pureftpdPath} - exit 1 -fi - -tar -xvf pure-ftpd-${pureftpdVersion}.tar.gz -rm -f pure-ftpd-${pureftpdVersion}.tar.gz -rm -f pure-ftpd-${pureftpdVersion}.tar.gz.checksum.txt -mv pure-ftpd-${pureftpdVersion} src -cd src - -./configure --prefix=${pureftpdPath} CFLAGS=-O2 --with-puredb --with-quotas --with-cookie --with-virtualhosts --with-diraliases --with-sysquotas --with-ratios --with-altlog --with-paranoidmsg --with-shadow --with-welcomemsg --with-throttling --with-uploadscript --with-language=simplified-chinese --with-rfc2640 --with-ftpwho --with-tls -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}编译配置失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make "-j${j}" -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}编译失败,请截图错误信息寻求帮助。" - exit 1 -fi - -make install -if [ ! -f "${pureftpdPath}/bin/pure-pw" ]; then - echo -e $HR - echo "错误:Pure-Ftpd-${pureftpdVersion}安装失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 还原配置 -cp /tmp/pureftpd.passwd ${pureftpdPath}/etc/pureftpd.passwd -cp /tmp/pureftpd.pdb ${pureftpdPath}/etc/pureftpd.pdb -cp /tmp/pureftpd.conf ${pureftpdPath}/etc/pureftpd.conf - -systemctl start pure-ftpd.service - -panel writePlugin pureftpd 1.0.50 - -echo -e "${HR}\nPure-Ftpd-${pureftpdVersion} 升级完成\n${HR}" diff --git a/scripts/redis/install.sh b/scripts/redis/install.sh deleted file mode 100644 index 98e20459..00000000 --- a/scripts/redis/install.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/redis" -setupPath="/www" -redisPath="${setupPath}/server/redis" -redisVersion="7.2.5" -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -if ! id -u "redis" > /dev/null 2>&1; then - groupadd redis - useradd -s /sbin/nologin -g redis redis -fi - -# 安装依赖 -if [ "${OS}" == "centos" ]; then - dnf makecache -y - dnf groupinstall "Development Tools" -y - dnf install systemd-devel openssl-devel -y -elif [ "${OS}" == "debian" ]; then - apt-get update - apt-get install build-essential libsystemd-dev libssl-dev -y -else - echo -e $HR - echo "错误:耗子面板不支持该系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装依赖软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 准备目录 -rm -rf ${redisPath} -mkdir -p ${redisPath} -cd ${redisPath} - -# 下载源码 -wget -T 120 -t 3 -O ${redisPath}/redis-${redisVersion}.tar.gz ${downloadUrl}/redis-${redisVersion}.tar.gz -wget -T 20 -t 3 -O ${redisPath}/redis-${redisVersion}.tar.gz.checksum.txt ${downloadUrl}/redis-${redisVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c redis-${redisVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:Redis源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${redisPath} - exit 1 -fi - -tar -zxvf redis-${redisVersion}.tar.gz -rm -f redis-${redisVersion}.tar.gz -rm -f redis-${redisVersion}.tar.gz.checksum.txt -mv redis-${redisVersion}/* ./ && rm -rf redis-${redisVersion} -mkdir -p ${redisPath}/bin - -make BUILD_TLS=yes USE_SYSTEMD=yes -j${cpuCore} -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Redis编译失败,请截图错误信息寻求帮助。" - rm -rf ${redisPath} - exit 1 -fi -make PREFIX=${redisPath} install -if [ ! -f "${redisPath}/bin/redis-server" ]; then - echo -e $HR - echo "错误:Redis安装失败,请截图错误信息寻求帮助。" - rm -rf ${redisPath} - exit 1 -fi - -# 设置软链接 -ln -sf ${redisPath}/bin/redis-cli /usr/bin/redis-cli -ln -sf ${redisPath}/bin/redis-server /usr/bin/redis-server -ln -sf ${redisPath}/bin/redis-sentinel /usr/bin/redis-sentinel -ln -sf ${redisPath}/bin/redis-benchmark /usr/bin/redis-benchmark -ln -sf ${redisPath}/bin/redis-check-aof /usr/bin/redis-check-aof -ln -sf ${redisPath}/bin/redis-check-rdb /usr/bin/redis-check-rdb - -# 设置配置文件 -VM_OVERCOMMIT_MEMORY=$(cat /etc/sysctl.conf|grep vm.overcommit_memory) -NET_CORE_SOMAXCONN=$(cat /etc/sysctl.conf|grep net.core.somaxconn) -if [ -z "${VM_OVERCOMMIT_MEMORY}" ] && [ -z "${NET_CORE_SOMAXCONN}" ];then - echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf - echo "net.core.somaxconn = 1024" >> /etc/sysctl.conf - sysctl -p -fi - -sed -i 's/dir .\//dir \/www\/server\/redis\//g' ${redisPath}/redis.conf -sed -i 's/# supervised.*/supervised systemd/g' ${redisPath}/redis.conf -sed -i 's/daemonize.*/daemonize no/g' ${redisPath}/redis.conf - -if [ "${ARCH}" == "aarch64" ]; then - echo "ignore-warnings ARM64-COW-BUG" >> ${redisPath}/redis.conf -fi - -chown -R redis:redis ${redisPath} -chmod -R 755 ${redisPath} - -# 设置服务 -cp -r utils/systemd-redis_server.service /etc/systemd/system/redis.service -sed -i "s!ExecStart=.*!ExecStart=${redisPath}/bin/redis-server ${redisPath}/redis.conf!g" /etc/systemd/system/redis.service -sed -i "s!#User=.*!User=redis!g" /etc/systemd/system/redis.service -sed -i "s!#Group=.*!Group=redis!g" /etc/systemd/system/redis.service -sed -i "s!#WorkingDirectory=.*!WorkingDirectory=${redisPath}!g" /etc/systemd/system/redis.service - -systemctl daemon-reload -systemctl enable redis -systemctl start redis - -panel writePlugin redis ${redisVersion} - -echo -e "${HR}\nRedis 安装完成\n${HR}" diff --git a/scripts/redis/uninstall.sh b/scripts/redis/uninstall.sh deleted file mode 100644 index 079b6bc4..00000000 --- a/scripts/redis/uninstall.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" - -systemctl stop redis -systemctl disable redis -rm -rf /etc/systemd/system/redis.service -systemctl daemon-reload -pkill -9 redis -rm -rf /www/server/redis - -rm -rf /usr/bin/redis-cli -rm -rf /usr/bin/redis-server -rm -rf /usr/bin/redis-benchmark -rm -rf /usr/bin/redis-check-aof -rm -rf /usr/bin/redis-check-rdb -rm -rf /usr/bin/redis-sentinel - -panel deletePlugin redis - -echo -e "${HR}\nRedis 卸载完成\n${HR}" diff --git a/scripts/redis/update.sh b/scripts/redis/update.sh deleted file mode 100644 index 326b287a..00000000 --- a/scripts/redis/update.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -ARCH=$(uname -m) -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -downloadUrl="https://dl.cdn.haozi.net/panel/redis" -setupPath="/www" -redisPath="${setupPath}/server/redis" -redisVersion="7.2.5" -cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l) - -# 读取信息 -redisConfig="${redisPath}/redis.conf" -redisPort=$(cat ${redisConfig} | grep 'port ' | grep -v '#' | awk '{print $2}') -redisPass=$(cat ${redisConfig} | grep 'requirepass ' | grep -v '#' | awk '{print $2}') -redisHost=$(cat ${redisConfig} | grep 'bind ' | grep -v '#' | awk '{print $2}') -redisDir=$(cat ${redisConfig} | grep 'dir ' | grep -v '#' | awk '{print $2}') - -# 备份 -if [ -z "${redisPass}" ]; then - redis-cli -p ${redisPort} << EOF -SAVE -EOF -else - redis-cli -p ${redisPort} -a ${redisPass} << EOF -SAVE -EOF -fi -mv ${redisDir}/dump.rdb /tmp/dump.rdb.bak - -# 准备目录 -rm -rf ${redisPath} -mkdir -p ${redisPath} -cd ${redisPath} - -# 下载源码 -wget -T 120 -t 3 -O ${redisPath}/redis-${redisVersion}.tar.gz ${downloadUrl}/redis-${redisVersion}.tar.gz -wget -T 20 -t 3 -O ${redisPath}/redis-${redisVersion}.tar.gz.checksum.txt ${downloadUrl}/redis-${redisVersion}.tar.gz.checksum.txt - -if ! sha256sum --status -c redis-${redisVersion}.tar.gz.checksum.txt; then - echo -e $HR - echo "错误:Redis源码 checksum 校验失败,文件可能被篡改或不完整,已终止操作" - rm -rf ${redisPath} - exit 1 -fi - -tar -zxvf redis-${redisVersion}.tar.gz -rm -f redis-${redisVersion}.tar.gz -rm -f redis-${redisVersion}.tar.gz.checksum.txt -mv redis-${redisVersion}/* ./ && rm -rf redis-${redisVersion} -mkdir -p ${redisPath}/bin - -make BUILD_TLS=yes USE_SYSTEMD=yes -j${cpuCore} -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:Redis编译失败,请截图错误信息寻求帮助。" - exit 1 -fi -make PREFIX=${redisPath} install -if [ ! -f "${redisPath}/bin/redis-server" ]; then - echo -e $HR - echo "错误:Redis升级失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 设置软链接 -ln -sf ${redisPath}/bin/redis-cli /usr/bin/redis-cli -ln -sf ${redisPath}/bin/redis-server /usr/bin/redis-server -ln -sf ${redisPath}/bin/redis-sentinel /usr/bin/redis-sentinel -ln -sf ${redisPath}/bin/redis-benchmark /usr/bin/redis-benchmark -ln -sf ${redisPath}/bin/redis-check-aof /usr/bin/redis-check-aof -ln -sf ${redisPath}/bin/redis-check-rdb /usr/bin/redis-check-rdb - -# 设置配置文件 -sed -i 's/dir .\//dir \/www\/server\/redis\//g' ${redisPath}/redis.conf -sed -i 's/# supervised.*/supervised systemd/g' ${redisPath}/redis.conf -sed -i 's/daemonize.*/daemonize no/g' ${redisPath}/redis.conf - -if [ "${ARCH}" == "aarch64" ]; then - echo "ignore-warnings ARM64-COW-BUG" >> ${redisPath}/redis.conf -fi - -# 恢复配置 -if [ -n "${redisPass}" ]; then - sed -i "s!# requirepass .*!requirepass ${redisPass}!g" ${redisPath}/redis.conf -fi -if [ -n "${redisHost}" ]; then - sed -i "s!bind .*!bind ${redisHost}!g" ${redisPath}/redis.conf -fi -if [ -n "${redisPort}" ]; then - sed -i "s!port .*!port ${redisPort}!g" ${redisPath}/redis.conf -fi -if [ -n "${redisDir}" ]; then - sed -i "s!dir .*!dir ${redisDir}!g" ${redisPath}/redis.conf -fi - -# 恢复数据 -if [ -f "/tmp/dump.rdb.bak" ]; then - mv /tmp/dump.rdb.bak ${redisDir}/dump.rdb -fi - -chown -R redis:redis ${redisPath} -chmod -R 755 ${redisPath} - -# 设置服务 -cp -r utils/systemd-redis_server.service /etc/systemd/system/redis.service -sed -i "s!ExecStart=.*!ExecStart=${redisPath}/bin/redis-server ${redisPath}/redis.conf!g" /etc/systemd/system/redis.service -sed -i "s!#User=.*!User=redis!g" /etc/systemd/system/redis.service -sed -i "s!#Group=.*!Group=redis!g" /etc/systemd/system/redis.service -sed -i "s!#WorkingDirectory=.*!WorkingDirectory=${redisPath}!g" /etc/systemd/system/redis.service - -systemctl daemon-reload -systemctl restart redis - -panel writePlugin redis ${redisVersion} - -echo -e "${HR}\nRedis 升级完成\n${HR}" diff --git a/scripts/rsync/install.sh b/scripts/rsync/install.sh deleted file mode 100644 index 680e9785..00000000 --- a/scripts/rsync/install.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf install -y rsync -elif [ "${OS}" == "debian" ]; then - apt-get install -y rsync -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -# 写入配置 -cat > /etc/rsyncd.conf << EOF -uid = root -gid = root -port = 873 -use chroot = no -read only = no -dont compress = *.jpg *.jpeg *.png *.gif *.webp *.avif *.mp4 *.avi *.mov *.mkv *.mp3 *.wav *.aac *.flac *.zip *.rar *.7z *.gz *.tgz *.tar *.pdf *.epub *.iso *.exe *.apk *.dmg *.rpm *.deb *.msi -hosts allow = 127.0.0.1/32 ::1/128 -# hosts deny = -max connections = 100 -timeout = 1800 -lock file = /var/run/rsync.lock -pid file = /var/run/rsyncd.pid -log file = /var/log/rsyncd.log - -EOF - -touch /etc/rsyncd.secrets -chmod 644 /etc/rsyncd.conf -chmod 600 /etc/rsyncd.secrets - -# 写入服务文件 -cat > /etc/systemd/system/rsyncd.service << EOF -[Unit] -Description=fast remote file copy program daemon -After=network-online.target remote-fs.target nss-lookup.target -Wants=network-online.target -ConditionPathExists=/etc/rsyncd.conf - -[Service] -ExecStart=/usr/bin/rsync --daemon --no-detach "\$OPTIONS" -ExecReload=/bin/kill -HUP \$MAINPID -KillMode=process -Restart=on-failure -RestartSec=5s - -[Install] -WantedBy=multi-user.target -EOF - -systemctl daemon-reload -systemctl enable rsyncd.service -systemctl restart rsyncd.service - -panel writePlugin rsync 3.2.7 diff --git a/scripts/rsync/uninstall.sh b/scripts/rsync/uninstall.sh deleted file mode 100644 index 912f38de..00000000 --- a/scripts/rsync/uninstall.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -systemctl stop rsync -systemctl disable rsync -rm -f /etc/systemd/system/rsyncd.service -systemctl daemon-reload - -if [ "${OS}" == "centos" ]; then - dnf remove -y rsync -elif [ "${OS}" == "debian" ]; then - apt-get purge -y rsync -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -# 删除配置 -rm -rf /etc/rsyncd.conf -rm -rf /etc/rsyncd.secrets - -panel deletePlugin rsync diff --git a/scripts/rsync/update.sh b/scripts/rsync/update.sh deleted file mode 100644 index b1ffe37d..00000000 --- a/scripts/rsync/update.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf update -y rsync -elif [ "${OS}" == "debian" ]; then - apt-get install --only-upgrade -y rsync -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel writePlugin rsync 3.2.7 diff --git a/scripts/s3fs/install.sh b/scripts/s3fs/install.sh deleted file mode 100644 index 642998d1..00000000 --- a/scripts/s3fs/install.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf install -y s3fs-fuse -elif [ "${OS}" == "debian" ]; then - apt-get install -y s3fs -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -panel writePlugin s3fs 1.9 diff --git a/scripts/s3fs/uninstall.sh b/scripts/s3fs/uninstall.sh deleted file mode 100644 index 9d806a3e..00000000 --- a/scripts/s3fs/uninstall.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf remove -y s3fs-fuse -elif [ "${OS}" == "debian" ]; then - apt-get purge -y s3fs -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel deletePlugin s3fs diff --git a/scripts/s3fs/update.sh b/scripts/s3fs/update.sh deleted file mode 100644 index da39845b..00000000 --- a/scripts/s3fs/update.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf update -y s3fs-fuse -elif [ "${OS}" == "debian" ]; then - apt-get install --only-upgrade -y s3fs -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel writePlugin s3fs 1.9 diff --git a/scripts/supervisor/install.sh b/scripts/supervisor/install.sh deleted file mode 100644 index 466eff31..00000000 --- a/scripts/supervisor/install.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf install -y supervisor - sed -i 's#files = supervisord.d/\*.ini#files = supervisord.d/*.conf#g' /etc/supervisord.conf - systemctl enable supervisord - systemctl start supervisord -elif [ "${OS}" == "debian" ]; then - apt-get install -y supervisor - systemctl enable supervisor - systemctl start supervisor -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi -if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:安装软件失败,请截图错误信息寻求帮助。" - exit 1 -fi - -panel writePlugin supervisor 4.2.5 diff --git a/scripts/supervisor/uninstall.sh b/scripts/supervisor/uninstall.sh deleted file mode 100644 index 797dff7e..00000000 --- a/scripts/supervisor/uninstall.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - systemctl stop supervisord - systemctl disable supervisord - dnf remove -y supervisor -elif [ "${OS}" == "debian" ]; then - systemctl stop supervisor - systemctl disable supervisor - apt-get purge -y supervisor -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel deletePlugin supervisor diff --git a/scripts/supervisor/update.sh b/scripts/supervisor/update.sh deleted file mode 100644 index e56b0169..00000000 --- a/scripts/supervisor/update.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") - -if [ "${OS}" == "centos" ]; then - dnf update -y supervisor -elif [ "${OS}" == "debian" ]; then - apt-get install --only-upgrade -y supervisor -else - echo -e $HR - echo "错误:不支持的操作系统" - exit 1 -fi - -panel writePlugin supervisor 4.2.5 diff --git a/scripts/uninstall_panel.sh b/scripts/uninstall_panel.sh deleted file mode 100644 index 436a78bb..00000000 --- a/scripts/uninstall_panel.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -LOGO="+----------------------------------------------------\n| 耗子面板卸载脚本\n+----------------------------------------------------\n| Copyright © 2022-"$(date +%Y)" 耗子科技 All rights reserved.\n+----------------------------------------------------" -HR="+----------------------------------------------------" -download_Url="" -setup_Path="/www" - -Prepare_System() { - if [ $(whoami) != "root" ]; then - echo -e $HR - echo "错误:请使用root用户运行卸载命令。" - exit 1 - fi - - isInstalled=$(systemctl status panel 2>&1 | grep "Active") - if [ "${isInstalled}" == "" ]; then - echo -e $HR - echo "错误:耗子面板未安装,无需卸载。" - exit 1 - fi - - if ! id -u "www" > /dev/null 2>&1; then - groupadd www - useradd -s /sbin/nologin -g www www - fi -} - -Remove_Swap() { - swapFile="${setup_Path}/swap" - if [ -f "${swapFile}" ]; then - swapoff ${swapFile} - rm -f ${swapFile} - sed -i '/swap/d' /etc/fstab - fi - - mount -a - if [ "$?" != "0" ]; then - echo -e $HR - echo "错误:检测到系统的 /etc/fstab 文件配置有误,请检查排除后重试,问题解决前勿重启系统。" - exit 1 - fi -} - -Remove_Panel() { - systemctl stop panel - systemctl disable panel - rm -f /etc/systemd/system/panel.service - rm -f /usr/bin/panel - rm -rf ${setup_Path} -} - -clear -echo -e "${LOGO}" - -# 卸载确认 -echo -e "高危操作,卸载面板前请务必备份好所有数据,提前卸载面板所有插件!" -echo -e "卸载面板后,所有数据将被清空,无法恢复!" -read -r -p "输入 y 并回车以确认卸载面板:" uninstall -if [ "${uninstall}" != 'y' ]; then - echo "输入不正确,已退出卸载。" - exit -fi - -echo -e "${LOGO}" -echo '正在卸载耗子面板...' -echo -e $HR - -Prepare_System -Remove_Swap -Remove_Panel - -clear - -echo -e "${LOGO}" -echo '耗子面板卸载完成。' -echo '感谢您的使用,欢迎您再次使用耗子面板。' -echo -e $HR - -rm -f uninstall_panel.sh -rm -f uninstall_panel.sh.checksum.txt diff --git a/scripts/update_panel.sh b/scripts/update_panel.sh deleted file mode 100644 index ff27a18e..00000000 --- a/scripts/update_panel.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/bash -export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH - -: ' -Copyright (C) 2022 - now HaoZi Technology Co., Ltd. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -' - -HR="+----------------------------------------------------" -OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown") -if [ "${OS}" == "unknown" ]; then - echo -e $HR - echo "错误:该系统不支持安装耗子面板,请更换 Debian 12.x / RHEL 9.x 安装。" - exit 1 -fi - -oldVersion=$(panel getSetting version) -oldVersion=${oldVersion#v} -panelPath="/www/panel" - -# 大于 -function version_gt() { test "$(echo -e "$1\n$2" | tr " " "\n" | sort -V | head -n 1)" != "$1"; } -# 小于 -function version_lt() { test "$(echo -e "$1\n$2" | tr " " "\n" | sort -rV | head -n 1)" != "$1"; } -# 大于等于 -function version_ge() { test "$(echo -e "$1\n$2" | tr " " "\n" | sort -rV | head -n 1)" == "$1"; } -# 小于等于 -function version_le() { test "$(echo -e "$1\n$2" | tr " " "\n" | sort -V | head -n 1)" == "$1"; } - -if [ -z "$oldVersion" ]; then - if [ -f "$panelPath/database/panel.db" ]; then - echo "DB_FILE=$panelPath/database/panel.db" >> $panelPath/panel.conf - oldVersion=$(panel getSetting version) - oldVersion=${oldVersion#v} - sed -i '/DB_FILE/d' $panelPath/panel.conf - else - echo "错误:无法获取面板版本" - echo "Error: can't get panel version" - exit 1 - fi -fi - -# 判断版本号是否合法 -versionPattern="^[0-9]+\.[0-9]+\.[0-9]+$" -if [[ ! $oldVersion =~ $versionPattern ]]; then - if [ -f "$panelPath/database/panel.db" ]; then - echo "DB_FILE=$panelPath/database/panel.db" >> $panelPath/panel.conf - oldVersion=$(panel getSetting version) - oldVersion=${oldVersion#v} - sed -i '/DB_FILE/d' $panelPath/panel.conf - else - echo "错误:面板版本号不合法" - echo "Error: panel version is illegal" - exit 1 - fi -fi - -echo $HR - -if version_lt "$oldVersion" "2.1.8"; then - echo "更新面板到 v2.1.8 ..." - echo "Update panel to v2.1.8 ..." - oldEntrance=$(panel getSetting entrance) - echo "APP_ENTRANCE=$oldEntrance" >> $panelPath/panel.conf - panel deleteSetting entrance -fi - -if version_lt "$oldVersion" "2.1.30"; then - echo "更新面板到 v2.1.30 ..." - echo "Update panel to v2.1.30 ..." - sed -i '/APP_HOST/d' $panelPath/panel.conf - echo "APP_SSL=false" >> $panelPath/panel.conf - mv $panelPath/database/panel.db $panelPath/storage/panel.db - openssl req -x509 -nodes -days 36500 -newkey ec:<(openssl ecparam -name secp384r1) -keyout $panelPath/storage/ssl.key -out $panelPath/storage/ssl.crt -subj "/C=CN/ST=Tianjin/L=Tianjin/O=HaoZi Technology Co., Ltd./OU=HaoZi Panel/CN=Panel" -fi - -if version_lt "$oldVersion" "2.2.0"; then - echo "更新面板到 v2.2.0 ..." - echo "Update panel to v2.2.0 ..." - echo "APP_LOCALE=zh_CN" >> $panelPath/panel.conf -fi - -if version_lt "$oldVersion" "2.2.4"; then - echo "更新面板到 v2.2.4 ..." - echo "Update panel to v2.2.4 ..." - if [ "${OS}" == "centos" ]; then - dnf makecache - dnf install -y p7zip p7zip-plugins rsyslog - systemctl enable rsyslog - systemctl start rsyslog - else - apt-get update -y - apt-get install -y p7zip p7zip-full - fi -fi - -if version_lt "$oldVersion" "2.2.10"; then - echo "更新面板到 v2.2.10 ..." - echo "Update panel to v2.2.10 ..." - if [ -f "/usr/bin/podman" ]; then - panel writePlugin podman 4.0.0 - if [ "${OS}" == "debian" ]; then - apt-get install containers-storage -y - cp /usr/share/containers/storage.conf /etc/containers/storage.conf - fi - systemctl enable podman - systemctl enable podman.socket - systemctl enable podman-restart - systemctl start podman - systemctl start podman.socket - systemctl start podman-restart - fi -fi - -if version_lt "$oldVersion" "2.2.14"; then - echo "更新面板到 v2.2.14 ..." - echo "Update panel to v2.2.14 ..." - if [ -f "/www/server/openresty/bin/openresty" ]; then - mkdir -p /www/server/vhost/acme - chmod -R 644 /www/server/vhost/acme - fi -fi - -if version_lt "$oldVersion" "2.2.16"; then - echo "更新面板到 v2.2.16 ..." - echo "Update panel to v2.2.16 ..." - if [ -f "/www/server/mysql/bin/mysql" ]; then - ln -sf /www/server/mysql/bin/* /usr/bin/ - rm -f /etc/profile.d/mysql.sh - source /etc/profile - fi -fi - -if version_lt "$oldVersion" "2.2.20"; then - echo "更新面板到 v2.2.20 ..." - echo "Update panel to v2.2.20 ..." - echo "SESSION_LIFETIME=120" >> $panelPath/panel.conf -fi - -echo $HR -echo "更新结束" -echo "Update finished" diff --git a/storage/.gitignore b/storage/.gitignore deleted file mode 100644 index 03e21671..00000000 --- a/storage/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -panel.db -cert.pem -key.pem \ No newline at end of file diff --git a/storage/README.md b/storage/README.md new file mode 100644 index 00000000..47cabd95 --- /dev/null +++ b/storage/README.md @@ -0,0 +1,3 @@ +# storage + +storage 目录存放应用运行时产生的文件,如上传的文件、缓存文件等。 \ No newline at end of file diff --git a/app/http/middleware/.gitignore b/storage/logs/.gitkeep similarity index 100% rename from app/http/middleware/.gitignore rename to storage/logs/.gitkeep diff --git a/storage/sessions/.gitkeep b/storage/sessions/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/storage/temp/.gitignore b/storage/temp/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/storage/temp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/tests/setting/setting_test.go b/tests/setting/setting_test.go deleted file mode 100644 index b34629c3..00000000 --- a/tests/setting/setting_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package setting - -import ( - "testing" - - "github.com/stretchr/testify/suite" - - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/tests" -) - -type SettingTestSuite struct { - suite.Suite - tests.TestCase - setting internal.Setting -} - -func TestSettingTestSuite(t *testing.T) { - suite.Run(t, &SettingTestSuite{ - setting: services.NewSettingImpl(), - }) -} - -func (s *SettingTestSuite) SetupTest() { - -} - -func (s *SettingTestSuite) TestGet() { - a := s.setting.Get("test") - b := s.setting.Get("test", "test") - s.Equal("", a) - s.Equal("test", b) -} - -func (s *SettingTestSuite) TestSet() { - err := s.setting.Set("test", "test") - s.Nil(err) - err = s.setting.Delete("test") - s.Nil(err) -} - -func (s *SettingTestSuite) TestDelete() { - err := s.setting.Set("test", "test") - s.Nil(err) - err = s.setting.Delete("test") - s.Nil(err) -} diff --git a/tests/test_case.go b/tests/test_case.go deleted file mode 100644 index 9ec12946..00000000 --- a/tests/test_case.go +++ /dev/null @@ -1,15 +0,0 @@ -package tests - -import ( - "github.com/goravel/framework/testing" - - "github.com/TheTNB/panel/v2/bootstrap" -) - -func init() { - bootstrap.Boot() -} - -type TestCase struct { - testing.TestCase -} diff --git a/tests/user/user_test.go b/tests/user/user_test.go deleted file mode 100644 index f29461f6..00000000 --- a/tests/user/user_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package user - -import ( - "testing" - - "github.com/goravel/framework/facades" - "github.com/stretchr/testify/suite" - - "github.com/TheTNB/panel/v2/app/models" - "github.com/TheTNB/panel/v2/internal" - "github.com/TheTNB/panel/v2/internal/services" - "github.com/TheTNB/panel/v2/tests" -) - -type UserTestSuite struct { - suite.Suite - tests.TestCase - user internal.User -} - -func TestUserTestSuite(t *testing.T) { - suite.Run(t, &UserTestSuite{ - user: services.NewUserImpl(), - }) -} - -func (s *UserTestSuite) SetupTest() { - -} - -func (s *UserTestSuite) TestCreate() { - user, err := s.user.Create("haozi", "123456") - s.Nil(err) - s.Equal("haozi", user.Username) - _, err = facades.Orm().Query().Where("username", "haozi").Delete(&models.User{}) - s.Nil(err) -} - -func (s *UserTestSuite) TestUpdate() { - user, err := s.user.Create("haozi", "123456") - s.Nil(err) - s.Equal("haozi", user.Username) - user.Username = "haozi2" - user, err = s.user.Update(user) - s.Nil(err) - s.Equal("haozi2", user.Username) - _, err = facades.Orm().Query().Where("username", "haozi").Delete(&models.User{}) - s.Nil(err) -} diff --git a/web/.env.development b/web/.env.development new file mode 100644 index 00000000..7714a753 --- /dev/null +++ b/web/.env.development @@ -0,0 +1,17 @@ +VITE_APP_TITLE = '耗子面板' + +# 资源公共路径,需要以 /开头和结尾 +VITE_PUBLIC_PATH = '/' + +# 是否hash路由模式 +VITE_USE_HASH = false + +# axios base api +VITE_BASE_API = '/api' + +# 是否启用代理(只对本地vite server生效) +VITE_USE_PROXY = true + +# 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod' +VITE_PROXY_TYPE = 'dev' + diff --git a/web/.env.production b/web/.env.production new file mode 100644 index 00000000..75a18902 --- /dev/null +++ b/web/.env.production @@ -0,0 +1,20 @@ +VITE_APP_TITLE = '耗子面板' + +# 资源公共路径,需要以 /开头和结尾 +VITE_PUBLIC_PATH = '/' + +# 是否hash路由模式 +VITE_USE_HASH = false + +# base api +VITE_BASE_API = '/api' + +# 是否启用压缩 +VITE_USE_COMPRESS = false + +# 压缩类型 +VITE_COMPRESS_TYPE = gzip + + + + diff --git a/web/.eslintrc-auto-import.json b/web/.eslintrc-auto-import.json new file mode 100644 index 00000000..ed79adcd --- /dev/null +++ b/web/.eslintrc-auto-import.json @@ -0,0 +1,76 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "EffectScope": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "InjectionKey": true, + "PropType": true, + "Ref": true, + "VNode": true, + "WritableComputedRef": true, + "computed": true, + "createApp": true, + "customRef": true, + "defineAsyncComponent": true, + "defineComponent": true, + "effectScope": true, + "getCurrentInstance": true, + "getCurrentScope": true, + "h": true, + "inject": true, + "isProxy": true, + "isReactive": true, + "isReadonly": true, + "isRef": true, + "markRaw": true, + "nextTick": true, + "onActivated": true, + "onBeforeMount": true, + "onBeforeRouteLeave": true, + "onBeforeRouteUpdate": true, + "onBeforeUnmount": true, + "onBeforeUpdate": true, + "onDeactivated": true, + "onErrorCaptured": true, + "onMounted": true, + "onRenderTracked": true, + "onRenderTriggered": true, + "onScopeDispose": true, + "onServerPrefetch": true, + "onUnmounted": true, + "onUpdated": true, + "provide": true, + "reactive": true, + "readonly": true, + "ref": true, + "resolveComponent": true, + "shallowReactive": true, + "shallowReadonly": true, + "shallowRef": true, + "toRaw": true, + "toRef": true, + "toRefs": true, + "toValue": true, + "triggerRef": true, + "unref": true, + "useAttrs": true, + "useCssModule": true, + "useCssVars": true, + "useLink": true, + "useRoute": true, + "useRouter": true, + "useSlots": true, + "watch": true, + "watchEffect": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "onWatcherCleanup": true, + "useId": true, + "useModel": true, + "useTemplateRef": true + } +} diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs new file mode 100644 index 00000000..5528a658 --- /dev/null +++ b/web/.eslintrc.cjs @@ -0,0 +1,17 @@ +/* eslint-env node */ +require('@rushstack/eslint-patch/modern-module-resolution') + +module.exports = { + root: true, + extends: [ + 'plugin:vue/vue3-essential', + 'eslint:recommended', + '@vue/eslint-config-typescript', + '@vue/eslint-config-prettier/skip-formatting', + '@unocss', + '.eslintrc-auto-import.json' + ], + parserOptions: { + ecmaVersion: 'latest' + } +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 00000000..531b8e9e --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,32 @@ +# 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 + + +# Editor directories and files +.vscode +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.env + +stats.html + +types/components.d.ts +types/auto-imports.d.ts diff --git a/web/.prettierrc.json b/web/.prettierrc.json new file mode 100644 index 00000000..66e23359 --- /dev/null +++ b/web/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "tabWidth": 2, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "none" +} \ No newline at end of file diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000..6c7acc25 --- /dev/null +++ b/web/README.md @@ -0,0 +1,11 @@ +# 耗子面板 + +这是耗子面板的前端部分,使用 Vue3 + Vite + UnoCSS + Naive UI 开发。 + +预了解更多请移步 [耗子面板](https://github.com/TheTNB/panel)。 + +# Rat Panel + +This is the frontend part of Rat Panel, developed using Vue3 + Vite + UnoCSS + Naive UI. + +For more information, please visit [Rat Panel](https://github.com/TheTNB/panel). diff --git a/web/build/config/define.ts b/web/build/config/define.ts new file mode 100644 index 00000000..9f5d81b7 --- /dev/null +++ b/web/build/config/define.ts @@ -0,0 +1,13 @@ +import dayjs from 'dayjs' + +/** + * * 此处定义的是全局常量,启动或打包后将添加到window中 + * https://vitejs.cn/config/#define + */ + +// 项目构建时间 +const _BUILD_TIME_ = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss')) + +export const viteDefine = { + _BUILD_TIME_ +} diff --git a/web/build/config/index.ts b/web/build/config/index.ts new file mode 100644 index 00000000..967ddda3 --- /dev/null +++ b/web/build/config/index.ts @@ -0,0 +1,2 @@ +export * from './define' +export * from './proxy' diff --git a/web/build/config/proxy.ts b/web/build/config/proxy.ts new file mode 100644 index 00000000..3c89f9c0 --- /dev/null +++ b/web/build/config/proxy.ts @@ -0,0 +1,16 @@ +import type { ProxyOptions } from 'vite' +import { getProxyConfig } from '../../settings/proxy-config' + +export function createViteProxy(isUseProxy = true, proxyType: ProxyType) { + if (!isUseProxy) return undefined + + const proxyConfig = getProxyConfig(proxyType) + const proxy: Record = { + [proxyConfig.prefix]: { + target: proxyConfig.target, + changeOrigin: true, + rewrite: (path: string) => path.replace(new RegExp(`^${proxyConfig.prefix}`), '') + } + } + return proxy +} diff --git a/web/build/plugins/html.ts b/web/build/plugins/html.ts new file mode 100644 index 00000000..e92cb54f --- /dev/null +++ b/web/build/plugins/html.ts @@ -0,0 +1,16 @@ +import { createHtmlPlugin } from 'vite-plugin-html' + +export function setupHtmlPlugin(viteEnv: ViteEnv) { + const { VITE_APP_TITLE } = viteEnv + + const htmlPlugin = createHtmlPlugin({ + minify: true, + inject: { + data: { + title: VITE_APP_TITLE + } + }, + viteNext: true + }) + return htmlPlugin +} diff --git a/web/build/plugins/index.ts b/web/build/plugins/index.ts new file mode 100644 index 00000000..cadc8b9e --- /dev/null +++ b/web/build/plugins/index.ts @@ -0,0 +1,28 @@ +import type { PluginOption } from 'vite' +import vue from '@vitejs/plugin-vue' +import unocss from 'unocss/vite' +import { visualizer } from 'rollup-plugin-visualizer' +import viteCompression from 'vite-plugin-compression' + +import unplugins from './unplugin' +import { setupHtmlPlugin } from './html' + +export function setupVitePlugins(viteEnv: ViteEnv, isBuild: boolean): PluginOption[] { + const plugins = [vue(), ...unplugins, unocss(), setupHtmlPlugin(viteEnv)] + + if (viteEnv.VITE_USE_COMPRESS) { + plugins.push(viteCompression({ algorithm: viteEnv.VITE_COMPRESS_TYPE || 'gzip' })) + } + + if (isBuild) { + plugins.push( + visualizer({ + open: true, + gzipSize: true, + brotliSize: true + }) + ) + } + + return plugins +} diff --git a/web/build/plugins/unplugin.ts b/web/build/plugins/unplugin.ts new file mode 100644 index 00000000..cc0534be --- /dev/null +++ b/web/build/plugins/unplugin.ts @@ -0,0 +1,33 @@ +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' + +/** + * * unplugin-icons插件,自动引入iconify图标 + * usage: https://github.com/antfu/unplugin-icons + * 图标库: https://icones.js.org/ + */ +import Icons from 'unplugin-icons/vite' +import IconsResolver from 'unplugin-icons/resolver' + +export default [ + AutoImport({ + imports: ['vue', 'vue-router'], + dts: 'types/auto-imports.d.ts', + eslintrc: { + enabled: true + } + }), + Icons({ + compiler: 'vue3', + scale: 1, + defaultClass: 'inline-block' + }), + Components({ + resolvers: [ + NaiveUiResolver(), + IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' }) + ], + dts: 'types/components.d.ts' + }) +] diff --git a/web/build/utils.ts b/web/build/utils.ts new file mode 100644 index 00000000..ccdc54c5 --- /dev/null +++ b/web/build/utils.ts @@ -0,0 +1,38 @@ +import path from 'node:path' + +/** + * * 项目根路径 + * @descrition 结尾不带/ + */ +export function getRootPath() { + return path.resolve(process.cwd()) +} + +/** + * * 项目src路径 + * @param srcName src目录名称(默认: "src") + * @descrition 结尾不带斜杠 + */ +export function getSrcPath(srcName = 'src') { + return path.resolve(getRootPath(), srcName) +} + +/** + * * 转换env配置 + * @param envOptions + * @descrition boolean和数字类型转换 + */ +export function convertEnv(envOptions: Record): ViteEnv { + const result: any = {} + if (!envOptions) return result + + for (const envKey in envOptions) { + let envVal = envOptions[envKey] + if (['true', 'false'].includes(envVal)) envVal = envVal === 'true' + + if (['VITE_PORT'].includes(envKey)) envVal = +envVal + + result[envKey] = envVal + } + return result +} diff --git a/web/env.d.ts b/web/env.d.ts new file mode 100644 index 00000000..d4945dbc --- /dev/null +++ b/web/env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 00000000..fbb3111b --- /dev/null +++ b/web/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + <%= title %> + + +
+ +
+ logo +
+
+
+
+
+
+
+
+
<%= title %>
+
+ +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 00000000..43daf4cf --- /dev/null +++ b/web/package.json @@ -0,0 +1,72 @@ +{ + "private": true, + "type": "module", + "repository": { + "url": "https://github.com/TheTNB/panel-frontend" + }, + "license": "MIT", + "author": { + "name": "HaoZi Tech", + "email": "admin@haozi.net", + "url": "https://git.haozi.net/org" + }, + "scripts": { + "dev": "vite", + "build": "run-p type-check build-only", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier --write src/" + }, + "dependencies": { + "@guolao/vue-monaco-editor": "^1.5.4", + "@vueuse/core": "^11.0.3", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.5.0", + "axios": "^1.7.7", + "crypto-js": "^4.2.0", + "dayjs": "^1.11.13", + "echarts": "^5.5.1", + "install": "^0.13.0", + "lodash-es": "^4.17.21", + "monaco-editor-nginx": "^2.0.2", + "pinia": "^2.2.2", + "vue": "^3.5.5", + "vue-echarts": "^7.0.3", + "vue-i18n": "^10.0.1", + "vue-router": "^4.4.5" + }, + "devDependencies": { + "@iconify/json": "^2.2.248", + "@iconify/vue": "^4.1.2", + "@rushstack/eslint-patch": "^1.10.4", + "@tsconfig/node20": "^20.1.4", + "@types/crypto-js": "^4.2.2", + "@types/lodash-es": "^4.17.12", + "@types/node": "^20.16.5", + "@unocss/eslint-config": "^0.62.3", + "@vitejs/plugin-vue": "^5.1.3", + "@vue/eslint-config-prettier": "^9.0.0", + "@vue/eslint-config-typescript": "^13.0.0", + "@vue/tsconfig": "^0.5.1", + "colord": "^2.9.3", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.28.0", + "naive-ui": "^2.39.0", + "npm-run-all2": "^6.2.3", + "prettier": "^3.3.3", + "rollup-plugin-visualizer": "^5.12.0", + "sass": "^1.78.0", + "typescript": "^5.5.4", + "unocss": "^0.62.3", + "unplugin-auto-import": "^0.18.3", + "unplugin-icons": "^0.19.3", + "unplugin-vue-components": "^0.27.4", + "vite": "^5.4.5", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-html": "^3.2.2", + "vite-plugin-mock": "^3.0.2", + "vue-tsc": "^2.1.6" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 00000000..6da11660 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,4618 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@guolao/vue-monaco-editor': + specifier: ^1.5.4 + version: 1.5.4(monaco-editor@0.51.0)(vue@3.5.6(typescript@5.6.2)) + '@vueuse/core': + specifier: ^11.0.3 + version: 11.1.0(vue@3.5.6(typescript@5.6.2)) + '@xterm/addon-fit': + specifier: ^0.10.0 + version: 0.10.0(@xterm/xterm@5.5.0) + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 + axios: + specifier: ^1.7.7 + version: 1.7.7 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + echarts: + specifier: ^5.5.1 + version: 5.5.1 + install: + specifier: ^0.13.0 + version: 0.13.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + monaco-editor-nginx: + specifier: ^2.0.2 + version: 2.0.2(@babel/runtime@7.25.6)(@nginx/reference-lib@1.1.2)(monaco-editor@0.51.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + pinia: + specifier: ^2.2.2 + version: 2.2.2(typescript@5.6.2)(vue@3.5.6(typescript@5.6.2)) + vue: + specifier: ^3.5.5 + version: 3.5.6(typescript@5.6.2) + vue-echarts: + specifier: ^7.0.3 + version: 7.0.3(@vue/runtime-core@3.5.6)(echarts@5.5.1)(vue@3.5.6(typescript@5.6.2)) + vue-i18n: + specifier: ^10.0.1 + version: 10.0.1(vue@3.5.6(typescript@5.6.2)) + vue-router: + specifier: ^4.4.5 + version: 4.4.5(vue@3.5.6(typescript@5.6.2)) + devDependencies: + '@iconify/json': + specifier: ^2.2.248 + version: 2.2.249 + '@iconify/vue': + specifier: ^4.1.2 + version: 4.1.2(vue@3.5.6(typescript@5.6.2)) + '@rushstack/eslint-patch': + specifier: ^1.10.4 + version: 1.10.4 + '@tsconfig/node20': + specifier: ^20.1.4 + version: 20.1.4 + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/node': + specifier: ^20.16.5 + version: 20.16.5 + '@unocss/eslint-config': + specifier: ^0.62.3 + version: 0.62.4(eslint@8.57.0)(typescript@5.6.2) + '@vitejs/plugin-vue': + specifier: ^5.1.3 + version: 5.1.3(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0))(vue@3.5.6(typescript@5.6.2)) + '@vue/eslint-config-prettier': + specifier: ^9.0.0 + version: 9.0.0(eslint@8.57.0)(prettier@3.3.3) + '@vue/eslint-config-typescript': + specifier: ^13.0.0 + version: 13.0.0(eslint-plugin-vue@9.28.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.6.2) + '@vue/tsconfig': + specifier: ^0.5.1 + version: 0.5.1 + colord: + specifier: ^2.9.3 + version: 2.9.3 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-plugin-vue: + specifier: ^9.28.0 + version: 9.28.0(eslint@8.57.0) + naive-ui: + specifier: ^2.39.0 + version: 2.39.0(vue@3.5.6(typescript@5.6.2)) + npm-run-all2: + specifier: ^6.2.3 + version: 6.2.3 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + rollup-plugin-visualizer: + specifier: ^5.12.0 + version: 5.12.0(rollup@4.21.3) + sass: + specifier: ^1.78.0 + version: 1.78.0 + typescript: + specifier: ^5.5.4 + version: 5.6.2 + unocss: + specifier: ^0.62.3 + version: 0.62.4(postcss@8.4.47)(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + unplugin-auto-import: + specifier: ^0.18.3 + version: 0.18.3(@vueuse/core@11.1.0(vue@3.5.6(typescript@5.6.2)))(rollup@4.21.3) + unplugin-icons: + specifier: ^0.19.3 + version: 0.19.3(@vue/compiler-sfc@3.5.6) + unplugin-vue-components: + specifier: ^0.27.4 + version: 0.27.4(@babel/parser@7.25.6)(rollup@4.21.3)(vue@3.5.6(typescript@5.6.2)) + vite: + specifier: ^5.4.5 + version: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + vite-plugin-html: + specifier: ^3.2.2 + version: 3.2.2(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + vite-plugin-mock: + specifier: ^3.0.2 + version: 3.0.2(esbuild@0.23.1)(mockjs@1.1.0)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + vue-tsc: + specifier: ^2.1.6 + version: 2.1.6(typescript@5.6.2) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@0.4.1': + resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==} + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.6': + resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.25.6': + resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.25.6': + resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} + engines: {node: '>=6.9.0'} + + '@css-render/plugin-bem@0.15.14': + resolution: {integrity: sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==} + peerDependencies: + css-render: ~0.15.14 + + '@css-render/vue3-ssr@0.15.14': + resolution: {integrity: sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==} + peerDependencies: + vue: ^3.0.11 + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.1': + resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.0': + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@guolao/vue-monaco-editor@1.5.4': + resolution: {integrity: sha512-eyBAqxJeDpV4mZYZSpNvh3xUgKCld5eEe0dBtjJhsy2+L0MB6PYFZ/FbPHNwskgp2RoIpfn1DLrIhXXE3lVbwQ==} + peerDependencies: + '@vue/composition-api': ^1.7.1 + monaco-editor: '>=0.43.0' + vue: ^2.6.14 || >=3.0.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + '@humanwhocodes/config-array@0.11.14': + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@iconify/json@2.2.249': + resolution: {integrity: sha512-nOvcrdep4qB8L3WedGWC6238rU+oYfqLpTfQp8uV0Avxg7aXPvl9rGW0vnaq53exSgfvhQ1h7JcVUwJUuDHrzQ==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.1.33': + resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==} + + '@iconify/vue@4.1.2': + resolution: {integrity: sha512-CQnYqLiQD5LOAaXhBrmj1mdL2/NCJvwcC4jtW2Z8ukhThiFkLDkutarTOV2trfc9EXqUqRs0KqXOL9pZ/IyysA==} + peerDependencies: + vue: '>=3' + + '@intlify/core-base@10.0.1': + resolution: {integrity: sha512-6kpRGjhos95ph7QmEtP4tnWFTW102s71CLQAQwfsIGqOAcoJhzcYFpzIQ0gKXzqAIXsMD/hwM5qJ4ewqMHw3gg==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@10.0.1': + resolution: {integrity: sha512-fPeykrcgVT5eOIlshTHiPCN8FV3AZyBOdMS3XaXzfQ6eL5wqfc29I/EdIv5YXVW5X8e/BgYeWjBC0Cuznsl/2g==} + engines: {node: '>= 16'} + + '@intlify/shared@10.0.1': + resolution: {integrity: sha512-b4h7IWdZl710DnAhET8lgfgZ4Y9A2IZx/gbli3Ec/zHtYCoPqLHmiM7kUNBrSZj7d/SSjcMMZHuz5I09x3PYZw==} + engines: {node: '>= 16'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@monaco-editor/loader@1.4.0': + resolution: {integrity: sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==} + peerDependencies: + monaco-editor: '>= 0.21.0 < 1' + + '@nginx/reference-lib@1.1.2': + resolution: {integrity: sha512-d8HeRHsIOPfVFOpDOi1XjiGxfYyyRmdPd+a+V9vDpyjBWGHVwSSOwzvuPGJtc4V4KK6ZfYHm15FD1etiKYS6vQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.25': + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.1.0': + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.21.3': + resolution: {integrity: sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.21.3': + resolution: {integrity: sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.21.3': + resolution: {integrity: sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.21.3': + resolution: {integrity: sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.21.3': + resolution: {integrity: sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.21.3': + resolution: {integrity: sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.21.3': + resolution: {integrity: sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.21.3': + resolution: {integrity: sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.21.3': + resolution: {integrity: sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.21.3': + resolution: {integrity: sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.21.3': + resolution: {integrity: sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.21.3': + resolution: {integrity: sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.21.3': + resolution: {integrity: sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.21.3': + resolution: {integrity: sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.21.3': + resolution: {integrity: sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.21.3': + resolution: {integrity: sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==} + cpu: [x64] + os: [win32] + + '@rushstack/eslint-patch@1.10.4': + resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + + '@tsconfig/node20@20.1.4': + resolution: {integrity: sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==} + + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.7': + resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} + + '@types/node@20.16.5': + resolution: {integrity: sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/scope-manager@8.6.0': + resolution: {integrity: sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/types@8.6.0': + resolution: {integrity: sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@8.6.0': + resolution: {integrity: sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/utils@8.6.0': + resolution: {integrity: sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/visitor-keys@8.6.0': + resolution: {integrity: sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@unocss/astro@0.62.4': + resolution: {integrity: sha512-98KfkbrNhBLx2+uYxMiGsldIeIZ6/PbL4yaGRHeHoiHd7p4HmIyCF+auYe4Psntx3Yr8kU+XSIAhGDYebvTidQ==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + vite: + optional: true + + '@unocss/cli@0.62.4': + resolution: {integrity: sha512-p4VyS40mzn4LCOkIsbIRzN0Zi50rRepesREi2S1+R4Kpvd4QFeeuxTuZNHEyi2uCboQ9ZWl1gfStCXIrNECwTg==} + engines: {node: '>=14'} + hasBin: true + + '@unocss/config@0.62.4': + resolution: {integrity: sha512-XKudKxxW8P44JvlIdS6HBpfE3qZA9rhbemy6/sb8HyZjKYjgeM9jx5yjk+9+4hXNma/KlwDXwjAqY29z0S0SrA==} + engines: {node: '>=14'} + + '@unocss/core@0.62.4': + resolution: {integrity: sha512-Cc+Vo6XlaQpyVejkJrrzzWtiK9pgMWzVVBpm9VCVtwZPUjD4GSc+g7VQCPXSsr7m03tmSuRySJx72QcASmauNQ==} + + '@unocss/eslint-config@0.62.4': + resolution: {integrity: sha512-bulYXf80MhTlN2oG/d1r23/FJTNiGxK/gG+p9nl/UeJZc5NOnes7fsqVscFTXDxVQGVIFgeKfch3DRmzlxHe9Q==} + engines: {node: '>=14'} + + '@unocss/eslint-plugin@0.62.4': + resolution: {integrity: sha512-L4pm8L96OvE99FK+fZHQBXxsu+B/yvhf471Mf5o3idaq+pzptfpZcKKRXCeQKSAYbC80IV4Fm1V5dFxOHbDdPg==} + engines: {node: '>=14'} + + '@unocss/extractor-arbitrary-variants@0.62.4': + resolution: {integrity: sha512-e4hJfBMyFr6T6dYSTTjNv9CQwaU1CVEKxDlYP0GpfSgxsV58pguID9j1mt0/XZD6LvEDzwxj9RTRWKpUSWqp+Q==} + + '@unocss/inspector@0.62.4': + resolution: {integrity: sha512-bRcnI99gZecNzrUr6kDMdwGHkhUuTPyvvadRdaOxHc9Ow3ANNyqymeFM1q5anZEUZt8h15TYN0mdyQyIWkU3zg==} + + '@unocss/postcss@0.62.4': + resolution: {integrity: sha512-kWdHy7UsSP4bDu8I7sCKeO0VuzvVpNHmn2rifK5gNstUx5dZ1H/SoyXTHx5sKtgfZBRzdNXFu2nZ3PzYGvEFbw==} + engines: {node: '>=14'} + peerDependencies: + postcss: ^8.4.21 + + '@unocss/preset-attributify@0.62.4': + resolution: {integrity: sha512-ei5nNT58GON9iyCGRRiIrphzyQbBIZ9iEqSBhIY0flcfi1uAPUXV32aO2slqJnWWAIwbRSb1GMpwYR8mmfuz8g==} + + '@unocss/preset-icons@0.62.4': + resolution: {integrity: sha512-n9m2nRTxyiw0sqOwSioO3rro0kaPW0JJzWlzcfdwQ+ZORNR5WyJL298fLXYUFbZG3EOF+zSPg6CMDWudKk/tlA==} + + '@unocss/preset-mini@0.62.4': + resolution: {integrity: sha512-1O+QpQFx7FT61aheAZEYemW5e4AGib8TFGm+rWLudKq2IBNnXHcS5xsq5QvqdC7rp9Dn3lnW5du6ijow5kCBuw==} + + '@unocss/preset-tagify@0.62.4': + resolution: {integrity: sha512-8b2Kcsvt93xu1JqDqcD3QvvW0L5rqvH7ev3BlNEVx6n8ayBqfB5HEd4ILKr7wSC90re+EnCgnMm7EP2FiQAJkw==} + + '@unocss/preset-typography@0.62.4': + resolution: {integrity: sha512-ZVh+NbcibMmD6ve8Deub/G+XAFcGPuzE2Fx/tMAfWfYlfyOAtrMxuL+AARMthpRxdE0JOtggXNTrJb0ZhGYl9g==} + + '@unocss/preset-uno@0.62.4': + resolution: {integrity: sha512-2S6+molIz8dH/al0nfkU7i/pMS0oERPr4k9iW80Byt4cKDIhh/0jhZrC83kgZRtCf5hclSBO4oCoMTi1JF7SBw==} + + '@unocss/preset-web-fonts@0.62.4': + resolution: {integrity: sha512-kaxgYBVyMdBlErseN8kWLiaS2N5OMlwg5ktAxUlei275fMoY7inQjOwppnjDVveJbN9SP6TcqqFpBIPfUayPkQ==} + + '@unocss/preset-wind@0.62.4': + resolution: {integrity: sha512-YOzfQ11AmAnl1ZkcWLMMxCdezLjRKavLNk38LumUMtcdsa0DAy+1JjTp+KEvVQAnD+Et/ld5X+YcBWJkVy5WFQ==} + + '@unocss/reset@0.62.4': + resolution: {integrity: sha512-CtxjeDgN39fY/eZDLIXN4wy7C8W7+SD+41AlzGVU5JwhcXmnb1XoDpOd2lzMxc/Yy3F5dIJt2+MRDj9RnpX9Ew==} + + '@unocss/rule-utils@0.62.4': + resolution: {integrity: sha512-XUwLbLUzL+VSHCJNK5QBHC9RbFehumge1/XJmsRfmh0+oxgJoO1gvEvxi57gYEmdJdMRJHRJZ66se6+cB0Ymvw==} + engines: {node: '>=14'} + + '@unocss/transformer-attributify-jsx@0.62.4': + resolution: {integrity: sha512-z9DDqS2DibDR9gno55diKfAVegeJ9uoyQXQhH3R0KY4YMF49N1fWy/t74gOiHtlPmvjQtDRZYgjgaMCc2w8oWg==} + + '@unocss/transformer-compile-class@0.62.4': + resolution: {integrity: sha512-8yadY9T7LToJwSsrmYU3rUKlnDgPGVRvON7z9g1IjUCmFCGx7Gpg84x9KpKUG6eUTshPQFUI0YUHocrYFevAEA==} + + '@unocss/transformer-directives@0.62.4': + resolution: {integrity: sha512-bq9ZDG6/mr6X2mAogAo0PBVrLSLT0900MPqnj/ixadYHc7mRpX+y6bc/1AgWytZIFYSdNzf7XDoquZuwf42Ucg==} + + '@unocss/transformer-variant-group@0.62.4': + resolution: {integrity: sha512-W1fxMc2Lzxu4E+6JBQEBzK+AwoCQYI+EL2FT2BCUsAno37f3JdnwFFEVscck0epSdmdtidsSLDognyX8h10r8A==} + + '@unocss/vite@0.62.4': + resolution: {integrity: sha512-JKq3V6bcevYl9X5Jl3p9crArbhzI8JVWQkOxKV2nGLFaqvnc47vMSDxlU4MUdRWp3aQvzDw132tcx27oSbrojw==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 + + '@vitejs/plugin-vue@5.1.3': + resolution: {integrity: sha512-3xbWsKEKXYlmX82aOHufFQVnkbMC/v8fLpWwh6hWOUrK5fbbtBh9Q/WWse27BFgSy2/e2c0fz5Scgya9h2GLhw==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.5': + resolution: {integrity: sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==} + + '@volar/source-map@2.4.5': + resolution: {integrity: sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==} + + '@volar/typescript@2.4.5': + resolution: {integrity: sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==} + + '@vue/compiler-core@3.5.6': + resolution: {integrity: sha512-r+gNu6K4lrvaQLQGmf+1gc41p3FO2OUJyWmNqaIITaJU6YFiV5PtQSFZt8jfztYyARwqhoCayjprC7KMvT3nRA==} + + '@vue/compiler-dom@3.5.6': + resolution: {integrity: sha512-xRXqxDrIqK8v8sSScpistyYH0qYqxakpsIvqMD2e5sV/PXQ1mTwtXp4k42yHK06KXxKSmitop9e45Ui/3BrTEw==} + + '@vue/compiler-sfc@3.5.6': + resolution: {integrity: sha512-pjWJ8Kj9TDHlbF5LywjVso+BIxCY5wVOLhkEXRhuCHDxPFIeX1zaFefKs8RYoHvkSMqRWt93a0f2gNJVJixHwg==} + + '@vue/compiler-ssr@3.5.6': + resolution: {integrity: sha512-VpWbaZrEOCqnmqjE83xdwegtr5qO/2OPUC6veWgvNqTJ3bYysz6vY3VqMuOijubuUYPRpG3OOKIh9TD0Stxb9A==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/eslint-config-prettier@9.0.0': + resolution: {integrity: sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==} + peerDependencies: + eslint: '>= 8.0.0' + prettier: '>= 3.0.0' + + '@vue/eslint-config-typescript@13.0.0': + resolution: {integrity: sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + eslint-plugin-vue: ^9.0.0 + typescript: '>=4.7.4' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@2.1.6': + resolution: {integrity: sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.6': + resolution: {integrity: sha512-shZ+KtBoHna5GyUxWfoFVBCVd7k56m6lGhk5e+J9AKjheHF6yob5eukssHRI+rzvHBiU1sWs/1ZhNbLExc5oYQ==} + + '@vue/runtime-core@3.5.6': + resolution: {integrity: sha512-FpFULR6+c2lI+m1fIGONLDqPQO34jxV8g6A4wBOgne8eSRHP6PQL27+kWFIx5wNhhjkO7B4rgtsHAmWv7qKvbg==} + + '@vue/runtime-dom@3.5.6': + resolution: {integrity: sha512-SDPseWre45G38ENH2zXRAHL1dw/rr5qp91lS4lt/nHvMr0MhsbCbihGAWLXNB/6VfFOJe2O+RBRkXU+CJF7/sw==} + + '@vue/server-renderer@3.5.6': + resolution: {integrity: sha512-zivnxQnOnwEXVaT9CstJ64rZFXMS5ZkKxCjDQKiMSvUhXRzFLWZVbaBiNF4HGDqGNNsTgmjcCSmU6TB/0OOxLA==} + peerDependencies: + vue: 3.5.6 + + '@vue/shared@3.5.6': + resolution: {integrity: sha512-eidH0HInnL39z6wAt6SFIwBrvGOpDWsDxlw3rCgo1B+CQ1781WzQUSU3YjxgdkcJo9Q8S6LmXTkvI+cLHGkQfA==} + + '@vue/tsconfig@0.5.1': + resolution: {integrity: sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==} + + '@vueuse/core@11.1.0': + resolution: {integrity: sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==} + + '@vueuse/metadata@11.1.0': + resolution: {integrity: sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==} + + '@vueuse/shared@11.1.0': + resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} + + '@xterm/addon-fit@0.10.0': + resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bundle-require@4.2.1: + resolution: {integrity: sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + + bundle-require@5.0.0: + resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + computeds@0.0.1: + resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.7: + resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + + connect-history-api-fallback@1.6.0: + resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==} + engines: {node: '>=0.8'} + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + + consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + css-render@0.15.14: + resolution: {integrity: sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==} + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.0.11: + resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + date-fns-tz@2.0.1: + resolution: {integrity: sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==} + peerDependencies: + date-fns: 2.x + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dotenv-expand@8.0.3: + resolution: {integrity: sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + echarts@5.5.1: + resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@9.28.0: + resolution: {integrity: sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + evtd@0.2.4: + resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fdir@6.3.0: + resolution: {integrity: sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + highlight.js@11.10.0: + resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==} + engines: {node: '>=12.0.0'} + + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + importx@0.4.4: + resolution: {integrity: sha512-Lo1pukzAREqrBnnHC+tj+lreMTAvyxtkKsMxLY8H15M/bvLl54p3YuoTI70Tz7Il0AsgSlD7Lrk/FaApRcBL7w==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + install@0.13.0: + resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} + engines: {node: '>= 0.10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + jiti@2.0.0-beta.3: + resolution: {integrity: sha512-pmfRbVRs/7khFrSAYnSiJ8C0D5GvzkE4Ey2pAvUcJsw1ly/p+7ut27jbJrjY79BpAJQJ4gXYFtK6d1Aub+9baQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.7.1: + resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} + + mockjs@1.1.0: + resolution: {integrity: sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==} + hasBin: true + + monaco-editor-nginx@2.0.2: + resolution: {integrity: sha512-F/qejb0w0hxIyxbu2JMCe+MhnOV7bxGbUynnyMXk7Cy25Na5fD99u5QqRr7WfRhIn0xcdhBEkJ/jikQsxVnEOA==} + peerDependencies: + '@babel/runtime': '>=7.10.0' + '@nginx/reference-lib': '>=1.1.0' + monaco-editor: '>=0.22.3' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + monaco-editor@0.51.0: + resolution: {integrity: sha512-xaGwVV1fq343cM7aOYB6lVE4Ugf0UyimdD/x5PWcWBMKENwectaEu77FAN7c5sFiyumqeJdX1RPTh1ocioyDjw==} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + naive-ui@2.39.0: + resolution: {integrity: sha512-5oUJzRG+rtLSH8eRU+fJvVYiQids2BxF9jp+fwGoAqHOptEINrBlgBu9uy+95RHE5FLJ7Q/z41o+qkoGnUrKxQ==} + peerDependencies: + vue: ^3.0.0 + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + + node-html-parser@5.4.2: + resolution: {integrity: sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-normalize-package-bin@3.0.1: + resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-run-all2@6.2.3: + resolution: {integrity: sha512-5RsxC7jEc/RjxOYBVdEfrJf5FsJ0pHA7jr2/OxrThXknajETCTYjigOCG3iaGjdYIKEQlDuCG0ir0T1HTva8pg==} + engines: {node: ^14.18.0 || ^16.13.0 || >=18.0.0, npm: '>= 8'} + hasBin: true + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + ofetch@1.3.4: + resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-manager-detector@0.2.0: + resolution: {integrity: sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@0.2.0: + resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pinia@2.2.2: + resolution: {integrity: sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==} + peerDependencies: + '@vue/composition-api': ^1.4.0 + typescript: '>=4.4.4' + vue: ^2.6.14 || ^3.3.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + typescript: + optional: true + + pkg-types@1.2.0: + resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.4.47: + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-package-json-fast@3.0.2: + resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup-plugin-visualizer@5.12.0: + resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rollup: + optional: true + + rollup@4.21.3: + resolution: {integrity: sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sass@1.78.0: + resolution: {integrity: sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + seemly@0.3.8: + resolution: {integrity: sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA==} + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + synckit@0.9.1: + resolution: {integrity: sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==} + engines: {node: ^14.18.0 || >=16.0.0} + + terser@5.32.0: + resolution: {integrity: sha512-v3Gtw3IzpBJ0ugkxEX8U0W6+TnPKRRCWGh1jC/iM/e3Ki5+qvO1L1EAZ56bZasc64aXHwRHNIQEzm6//i5cemQ==} + engines: {node: '>=10'} + hasBin: true + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tinyexec@0.3.0: + resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + + tinyglobby@0.2.6: + resolution: {integrity: sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==} + engines: {node: '>=12.0.0'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + treemate@0.3.11: + resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + tsx@4.19.1: + resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.5.4: + resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + + unconfig@0.5.5: + resolution: {integrity: sha512-VQZ5PT9HDX+qag0XdgQi8tJepPhXiR/yVOkn707gJDKo31lGjRilPREiQJ9Z6zd/Ugpv6ZvO5VxVIcatldYcNQ==} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unimport@3.12.0: + resolution: {integrity: sha512-5y8dSvNvyevsnw4TBQkIQR1Rjdbb+XjVSwQwxltpnVZrStBvvPkMPcZrh1kg5kY77kpx6+D4Ztd3W6FOBH/y2Q==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unocss@0.62.4: + resolution: {integrity: sha512-SaGbxXQkk8GDPeJpWsBCZ8a23Knu4ixVTt6pvcQWKjOCGTd9XBd+vLZzN2WwdwgBPVwmMmx5wp+/gPHKFNOmIw==} + engines: {node: '>=14'} + peerDependencies: + '@unocss/webpack': 0.62.4 + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 + peerDependenciesMeta: + '@unocss/webpack': + optional: true + vite: + optional: true + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin-auto-import@0.18.3: + resolution: {integrity: sha512-q3FUtGQjYA2e+kb1WumyiQMjHM27MrTQ05QfVwtLRVhyYe+KF6TblBYaEX9L6Z0EibsqaXAiW+RFfkcQpfaXzg==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^3.2.2 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-icons@0.19.3: + resolution: {integrity: sha512-EUegRmsAI6+rrYr0vXjFlIP+lg4fSC4zb62zAZKx8FGXlWAGgEGBCa3JDe27aRAXhistObLPbBPhwa/0jYLFkQ==} + peerDependencies: + '@svgr/core': '>=7.0.0' + '@svgx/core': ^1.0.1 + '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 + vue-template-compiler: ^2.6.12 + vue-template-es2015-compiler: ^1.9.0 + peerDependenciesMeta: + '@svgr/core': + optional: true + '@svgx/core': + optional: true + '@vue/compiler-sfc': + optional: true + vue-template-compiler: + optional: true + vue-template-es2015-compiler: + optional: true + + unplugin-vue-components@0.27.4: + resolution: {integrity: sha512-1XVl5iXG7P1UrOMnaj2ogYa5YTq8aoh5jwDPQhemwO/OrXW+lPQKDXd1hMz15qxQPxgb/XXlbgo3HQ2rLEbmXQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + + unplugin@1.14.1: + resolution: {integrity: sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==} + engines: {node: '>=14.0.0'} + peerDependencies: + webpack-sources: ^3 + peerDependenciesMeta: + webpack-sources: + optional: true + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + vdirs@0.1.8: + resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==} + peerDependencies: + vue: ^3.0.11 + + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-html@3.2.2: + resolution: {integrity: sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-mock@3.0.2: + resolution: {integrity: sha512-bD//HvkTygGmk+LsIAdf0jGNlCv4iWv0kZlH9UEgWT6QYoUwfjQAE4SKxHRw2tfLgVhbPQVv/+X3YlNWvueGUA==} + engines: {node: '>=16.0.0'} + peerDependencies: + esbuild: '>=0.17' + mockjs: '>=1.1.0' + vite: '>=4.0.0' + + vite@5.4.6: + resolution: {integrity: sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vooks@0.2.12: + resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==} + peerDependencies: + vue: ^3.0.0 + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + vue-demi@0.13.11: + resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-echarts@7.0.3: + resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==} + peerDependencies: + '@vue/runtime-core': ^3.0.0 + echarts: ^5.5.1 + vue: ^2.7.0 || ^3.1.1 + peerDependenciesMeta: + '@vue/runtime-core': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-i18n@10.0.1: + resolution: {integrity: sha512-SQVlSm/1S6AaG1wexvwq3ebXUrrkx75ZHD78UAs4/rYD/X3tsQxfm6ElpT4ZPegJQEgRtOJjGripqSrfqAENtg==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-router@4.4.5: + resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==} + peerDependencies: + vue: ^3.2.0 + + vue-tsc@2.1.6: + resolution: {integrity: sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.6: + resolution: {integrity: sha512-zv+20E2VIYbcJOzJPUWp03NOGFhMmpCKOfSxVTmCYyYFFko48H9tmuQFzYj7tu4qX1AeXlp9DmhIP89/sSxxhw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vueuc@0.4.58: + resolution: {integrity: sha512-Wnj/N8WbPRSxSt+9ji1jtDHPzda5h2OH/0sFBhvdxDRuyCZbjGg3/cKMaKqEoe+dErTexG2R+i6Q8S/Toq1MYg==} + peerDependencies: + vue: ^3.0.11 + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zrender@5.6.0: + resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@antfu/install-pkg@0.4.1': + dependencies: + package-manager-detector: 0.2.0 + tinyexec: 0.3.0 + + '@antfu/utils@0.7.10': {} + + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/parser@7.25.6': + dependencies: + '@babel/types': 7.25.6 + + '@babel/runtime@7.25.6': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/types@7.25.6': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@css-render/plugin-bem@0.15.14(css-render@0.15.14)': + dependencies: + css-render: 0.15.14 + + '@css-render/vue3-ssr@0.15.14(vue@3.5.6(typescript@5.6.2))': + dependencies: + vue: 3.5.6(typescript@5.6.2) + + '@emotion/hash@0.8.0': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.23.1': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.23.1': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.23.1': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.23.1': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.23.1': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.23.1': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.23.1': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.23.1': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.23.1': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.23.1': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.23.1': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.23.1': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.23.1': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.23.1': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.23.1': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.23.1': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.23.1': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.23.1': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.23.1': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.23.1': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.23.1': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.23.1': + optional: true + + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.0': {} + + '@guolao/vue-monaco-editor@1.5.4(monaco-editor@0.51.0)(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@monaco-editor/loader': 1.4.0(monaco-editor@0.51.0) + monaco-editor: 0.51.0 + vue: 3.5.6(typescript@5.6.2) + vue-demi: 0.14.10(vue@3.5.6(typescript@5.6.2)) + + '@humanwhocodes/config-array@0.11.14': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@iconify/json@2.2.249': + dependencies: + '@iconify/types': 2.0.0 + pathe: 1.1.2 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.1.33': + dependencies: + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/types': 2.0.0 + debug: 4.3.7 + kolorist: 1.8.0 + local-pkg: 0.5.0 + mlly: 1.7.1 + transitivePeerDependencies: + - supports-color + + '@iconify/vue@4.1.2(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.6(typescript@5.6.2) + + '@intlify/core-base@10.0.1': + dependencies: + '@intlify/message-compiler': 10.0.1 + '@intlify/shared': 10.0.1 + + '@intlify/message-compiler@10.0.1': + dependencies: + '@intlify/shared': 10.0.1 + source-map-js: 1.2.1 + + '@intlify/shared@10.0.1': {} + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@juggle/resize-observer@3.4.0': {} + + '@monaco-editor/loader@1.4.0(monaco-editor@0.51.0)': + dependencies: + monaco-editor: 0.51.0 + state-local: 1.0.7 + + '@nginx/reference-lib@1.1.2': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@pkgr/core@0.1.1': {} + + '@polka/url@1.0.0-next.25': {} + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + + '@rollup/pluginutils@5.1.0(rollup@4.21.3)': + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + optionalDependencies: + rollup: 4.21.3 + + '@rollup/rollup-android-arm-eabi@4.21.3': + optional: true + + '@rollup/rollup-android-arm64@4.21.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.21.3': + optional: true + + '@rollup/rollup-darwin-x64@4.21.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.21.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.21.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.21.3': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.21.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.21.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.21.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.21.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.21.3': + optional: true + + '@rushstack/eslint-patch@1.10.4': {} + + '@tsconfig/node20@20.1.4': {} + + '@types/crypto-js@4.2.2': {} + + '@types/estree@1.0.5': {} + + '@types/katex@0.16.7': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.7 + + '@types/lodash@4.17.7': {} + + '@types/node@20.16.5': + dependencies: + undici-types: 6.19.8 + + '@types/web-bluetooth@0.0.20': {} + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@eslint-community/regexpp': 4.11.1 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.3.7 + eslint: 8.57.0 + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/scope-manager@8.6.0': + dependencies: + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/visitor-keys': 8.6.0 + + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + debug: 4.3.7 + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/types@8.6.0': {} + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.6.2)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.3.7 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.6.0(typescript@5.6.2)': + dependencies: + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/visitor-keys': 8.6.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@7.18.0(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.2) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.6.0(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@typescript-eslint/scope-manager': 8.6.0 + '@typescript-eslint/types': 8.6.0 + '@typescript-eslint/typescript-estree': 8.6.0(typescript@5.6.2) + eslint: 8.57.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@8.6.0': + dependencies: + '@typescript-eslint/types': 8.6.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.2.0': {} + + '@unocss/astro@0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0))': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/reset': 0.62.4 + '@unocss/vite': 0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + optionalDependencies: + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - rollup + - supports-color + + '@unocss/cli@0.62.4(rollup@4.21.3)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + '@unocss/config': 0.62.4 + '@unocss/core': 0.62.4 + '@unocss/preset-uno': 0.62.4 + cac: 6.7.14 + chokidar: 3.6.0 + colorette: 2.0.20 + consola: 3.2.3 + magic-string: 0.30.11 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + tinyglobby: 0.2.6 + transitivePeerDependencies: + - rollup + - supports-color + + '@unocss/config@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + unconfig: 0.5.5 + transitivePeerDependencies: + - supports-color + + '@unocss/core@0.62.4': {} + + '@unocss/eslint-config@0.62.4(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@unocss/eslint-plugin': 0.62.4(eslint@8.57.0)(typescript@5.6.2) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@unocss/eslint-plugin@0.62.4(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/utils': 8.6.0(eslint@8.57.0)(typescript@5.6.2) + '@unocss/config': 0.62.4 + '@unocss/core': 0.62.4 + magic-string: 0.30.11 + synckit: 0.9.1 + transitivePeerDependencies: + - eslint + - supports-color + - typescript + + '@unocss/extractor-arbitrary-variants@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/inspector@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/rule-utils': 0.62.4 + gzip-size: 6.0.0 + sirv: 2.0.4 + + '@unocss/postcss@0.62.4(postcss@8.4.47)': + dependencies: + '@unocss/config': 0.62.4 + '@unocss/core': 0.62.4 + '@unocss/rule-utils': 0.62.4 + css-tree: 2.3.1 + postcss: 8.4.47 + tinyglobby: 0.2.6 + transitivePeerDependencies: + - supports-color + + '@unocss/preset-attributify@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/preset-icons@0.62.4': + dependencies: + '@iconify/utils': 2.1.33 + '@unocss/core': 0.62.4 + ofetch: 1.3.4 + transitivePeerDependencies: + - supports-color + + '@unocss/preset-mini@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/extractor-arbitrary-variants': 0.62.4 + '@unocss/rule-utils': 0.62.4 + + '@unocss/preset-tagify@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/preset-typography@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/preset-mini': 0.62.4 + + '@unocss/preset-uno@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/preset-mini': 0.62.4 + '@unocss/preset-wind': 0.62.4 + '@unocss/rule-utils': 0.62.4 + + '@unocss/preset-web-fonts@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + ofetch: 1.3.4 + + '@unocss/preset-wind@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/preset-mini': 0.62.4 + '@unocss/rule-utils': 0.62.4 + + '@unocss/reset@0.62.4': {} + + '@unocss/rule-utils@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + magic-string: 0.30.11 + + '@unocss/transformer-attributify-jsx@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/transformer-compile-class@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/transformer-directives@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + '@unocss/rule-utils': 0.62.4 + css-tree: 2.3.1 + + '@unocss/transformer-variant-group@0.62.4': + dependencies: + '@unocss/core': 0.62.4 + + '@unocss/vite@0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + '@unocss/config': 0.62.4 + '@unocss/core': 0.62.4 + '@unocss/inspector': 0.62.4 + chokidar: 3.6.0 + magic-string: 0.30.11 + tinyglobby: 0.2.6 + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - rollup + - supports-color + + '@vitejs/plugin-vue@5.1.3(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0))(vue@3.5.6(typescript@5.6.2))': + dependencies: + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + vue: 3.5.6(typescript@5.6.2) + + '@volar/language-core@2.4.5': + dependencies: + '@volar/source-map': 2.4.5 + + '@volar/source-map@2.4.5': {} + + '@volar/typescript@2.4.5': + dependencies: + '@volar/language-core': 2.4.5 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + + '@vue/compiler-core@3.5.6': + dependencies: + '@babel/parser': 7.25.6 + '@vue/shared': 3.5.6 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.6': + dependencies: + '@vue/compiler-core': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/compiler-sfc@3.5.6': + dependencies: + '@babel/parser': 7.25.6 + '@vue/compiler-core': 3.5.6 + '@vue/compiler-dom': 3.5.6 + '@vue/compiler-ssr': 3.5.6 + '@vue/shared': 3.5.6 + estree-walker: 2.0.2 + magic-string: 0.30.11 + postcss: 8.4.47 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.6': + dependencies: + '@vue/compiler-dom': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/eslint-config-prettier@9.0.0(eslint@8.57.0)(prettier@3.3.3)': + dependencies: + eslint: 8.57.0 + eslint-config-prettier: 9.1.0(eslint@8.57.0) + eslint-plugin-prettier: 5.2.1(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.3) + prettier: 3.3.3 + transitivePeerDependencies: + - '@types/eslint' + + '@vue/eslint-config-typescript@13.0.0(eslint-plugin-vue@9.28.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.2))(eslint@8.57.0)(typescript@5.6.2) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.6.2) + eslint: 8.57.0 + eslint-plugin-vue: 9.28.0(eslint@8.57.0) + vue-eslint-parser: 9.4.3(eslint@8.57.0) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color + + '@vue/language-core@2.1.6(typescript@5.6.2)': + dependencies: + '@volar/language-core': 2.4.5 + '@vue/compiler-dom': 3.5.6 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.6 + computeds: 0.0.1 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.2 + + '@vue/reactivity@3.5.6': + dependencies: + '@vue/shared': 3.5.6 + + '@vue/runtime-core@3.5.6': + dependencies: + '@vue/reactivity': 3.5.6 + '@vue/shared': 3.5.6 + + '@vue/runtime-dom@3.5.6': + dependencies: + '@vue/reactivity': 3.5.6 + '@vue/runtime-core': 3.5.6 + '@vue/shared': 3.5.6 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.6(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@vue/compiler-ssr': 3.5.6 + '@vue/shared': 3.5.6 + vue: 3.5.6(typescript@5.6.2) + + '@vue/shared@3.5.6': {} + + '@vue/tsconfig@0.5.1': {} + + '@vueuse/core@11.1.0(vue@3.5.6(typescript@5.6.2))': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 11.1.0 + '@vueuse/shared': 11.1.0(vue@3.5.6(typescript@5.6.2)) + vue-demi: 0.14.10(vue@3.5.6(typescript@5.6.2)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@11.1.0': {} + + '@vueuse/shared@11.1.0(vue@3.5.6(typescript@5.6.2))': + dependencies: + vue-demi: 0.14.10(vue@3.5.6(typescript@5.6.2)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/xterm@5.5.0': {} + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + async-validator@4.2.5: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-from@1.1.2: {} + + bundle-require@4.2.1(esbuild@0.23.1): + dependencies: + esbuild: 0.23.1 + load-tsconfig: 0.2.5 + + bundle-require@5.0.0(esbuild@0.23.1): + dependencies: + esbuild: 0.23.1 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.7.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@12.1.0: {} + + commander@2.20.3: {} + + commander@8.3.0: {} + + computeds@0.0.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.7: {} + + connect-history-api-fallback@1.6.0: {} + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + consola@2.15.3: {} + + consola@3.2.3: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-js@4.2.0: {} + + css-render@0.15.14: + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.0.11 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.1.0: {} + + cssesc@3.0.0: {} + + csstype@3.0.11: {} + + csstype@3.1.3: {} + + date-fns-tz@2.0.1(date-fns@2.30.0): + dependencies: + date-fns: 2.30.0 + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.25.6 + + dayjs@1.11.13: {} + + de-indent@1.0.2: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-lazy-prop@2.0.0: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + destr@2.0.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@2.3.0: {} + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.7.0 + + dotenv-expand@8.0.3: {} + + dotenv@16.4.5: {} + + duplexer@0.1.2: {} + + echarts@5.5.1: + dependencies: + tslib: 2.3.0 + zrender: 5.6.0 + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: {} + + entities@2.2.0: {} + + entities@4.5.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-prettier@9.1.0(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + + eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.3): + dependencies: + eslint: 8.57.0 + prettier: 3.3.3 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.1 + optionalDependencies: + eslint-config-prettier: 9.1.0(eslint@8.57.0) + + eslint-plugin-vue@9.28.0(eslint@8.57.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + eslint: 8.57.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@8.57.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.7 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.5 + + esutils@2.0.3: {} + + evtd@0.2.4: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fdir@6.3.0(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.3.1: {} + + follow-redirects@1.15.9: {} + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + has-flag@4.0.0: {} + + he@1.2.0: {} + + highlight.js@11.10.0: {} + + html-minifier-terser@6.1.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.32.0 + + ignore@5.3.2: {} + + immutable@4.3.7: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + importx@0.4.4: + dependencies: + bundle-require: 5.0.0(esbuild@0.23.1) + debug: 4.3.7 + esbuild: 0.23.1 + jiti: 2.0.0-beta.3 + jiti-v1: jiti@1.21.6 + pathe: 1.1.2 + tsx: 4.19.1 + transitivePeerDependencies: + - supports-color + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + install@0.13.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jiti@1.21.6: {} + + jiti@2.0.0-beta.3: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@3.0.2: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + load-tsconfig@0.2.5: {} + + local-pkg@0.5.0: + dependencies: + mlly: 1.7.1 + pkg-types: 1.2.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.7.0 + + magic-string@0.30.11: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + mdn-data@2.0.30: {} + + memorystream@0.3.1: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + mlly@1.7.1: + dependencies: + acorn: 8.12.1 + pathe: 1.1.2 + pkg-types: 1.2.0 + ufo: 1.5.4 + + mockjs@1.1.0: + dependencies: + commander: 12.1.0 + + monaco-editor-nginx@2.0.2(@babel/runtime@7.25.6)(@nginx/reference-lib@1.1.2)(monaco-editor@0.51.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.6 + '@nginx/reference-lib': 1.1.2 + monaco-editor: 0.51.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + monaco-editor@0.51.0: {} + + mrmime@2.0.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + naive-ui@2.39.0(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@css-render/plugin-bem': 0.15.14(css-render@0.15.14) + '@css-render/vue3-ssr': 0.15.14(vue@3.5.6(typescript@5.6.2)) + '@types/katex': 0.16.7 + '@types/lodash': 4.17.7 + '@types/lodash-es': 4.17.12 + async-validator: 4.2.5 + css-render: 0.15.14 + csstype: 3.1.3 + date-fns: 2.30.0 + date-fns-tz: 2.0.1(date-fns@2.30.0) + evtd: 0.2.4 + highlight.js: 11.10.0 + lodash: 4.17.21 + lodash-es: 4.17.21 + seemly: 0.3.8 + treemate: 0.3.11 + vdirs: 0.1.8(vue@3.5.6(typescript@5.6.2)) + vooks: 0.2.12(vue@3.5.6(typescript@5.6.2)) + vue: 3.5.6(typescript@5.6.2) + vueuc: 0.4.58(vue@3.5.6(typescript@5.6.2)) + + nanoid@3.3.7: {} + + natural-compare@1.4.0: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.7.0 + + node-fetch-native@1.6.4: {} + + node-html-parser@5.4.2: + dependencies: + css-select: 4.3.0 + he: 1.2.0 + + normalize-path@3.0.0: {} + + npm-normalize-package-bin@3.0.1: {} + + npm-run-all2@6.2.3: + dependencies: + ansi-styles: 6.2.1 + cross-spawn: 7.0.3 + memorystream: 0.3.1 + minimatch: 9.0.5 + pidtree: 0.6.0 + read-package-json-fast: 3.0.2 + shell-quote: 1.8.1 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + ofetch@1.3.4: + dependencies: + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.4 + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-manager-detector@0.2.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.7.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.7.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-to-regexp@6.3.0: {} + + path-type@4.0.0: {} + + pathe@0.2.0: {} + + pathe@1.1.2: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.0: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pidtree@0.6.0: {} + + pinia@2.2.2(typescript@5.6.2)(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.6(typescript@5.6.2) + vue-demi: 0.14.10(vue@3.5.6(typescript@5.6.2)) + optionalDependencies: + typescript: 5.6.2 + + pkg-types@1.2.0: + dependencies: + confbox: 0.1.7 + mlly: 1.7.1 + pathe: 1.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.4.47: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.0 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.3.3: {} + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-package-json-fast@3.0.2: + dependencies: + json-parse-even-better-errors: 3.0.2 + npm-normalize-package-bin: 3.0.1 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + regenerator-runtime@0.14.1: {} + + relateurl@0.2.7: {} + + require-directory@2.1.1: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup-plugin-visualizer@5.12.0(rollup@4.21.3): + dependencies: + open: 8.4.2 + picomatch: 2.3.1 + source-map: 0.7.4 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.21.3 + + rollup@4.21.3: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.21.3 + '@rollup/rollup-android-arm64': 4.21.3 + '@rollup/rollup-darwin-arm64': 4.21.3 + '@rollup/rollup-darwin-x64': 4.21.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.21.3 + '@rollup/rollup-linux-arm-musleabihf': 4.21.3 + '@rollup/rollup-linux-arm64-gnu': 4.21.3 + '@rollup/rollup-linux-arm64-musl': 4.21.3 + '@rollup/rollup-linux-powerpc64le-gnu': 4.21.3 + '@rollup/rollup-linux-riscv64-gnu': 4.21.3 + '@rollup/rollup-linux-s390x-gnu': 4.21.3 + '@rollup/rollup-linux-x64-gnu': 4.21.3 + '@rollup/rollup-linux-x64-musl': 4.21.3 + '@rollup/rollup-win32-arm64-msvc': 4.21.3 + '@rollup/rollup-win32-ia32-msvc': 4.21.3 + '@rollup/rollup-win32-x64-msvc': 4.21.3 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sass@1.78.0: + dependencies: + chokidar: 3.6.0 + immutable: 4.3.7 + source-map-js: 1.2.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + scule@1.3.0: {} + + seemly@0.3.8: {} + + semver@7.6.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.1: {} + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + + slash@3.0.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + state-local@1.0.7: {} + + statuses@1.5.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + strip-literal@2.1.0: + dependencies: + js-tokens: 9.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + synckit@0.9.1: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.7.0 + + terser@5.32.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-table@0.2.0: {} + + tinyexec@0.3.0: {} + + tinyglobby@0.2.6: + dependencies: + fdir: 6.3.0(picomatch@4.0.2) + picomatch: 4.0.2 + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + treemate@0.3.11: {} + + ts-api-utils@1.3.0(typescript@5.6.2): + dependencies: + typescript: 5.6.2 + + tslib@2.3.0: {} + + tslib@2.7.0: {} + + tsx@4.19.1: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typescript@5.6.2: {} + + ufo@1.5.4: {} + + unconfig@0.5.5: + dependencies: + '@antfu/utils': 0.7.10 + defu: 6.1.4 + importx: 0.4.4 + transitivePeerDependencies: + - supports-color + + undici-types@6.19.8: {} + + unimport@3.12.0(rollup@4.21.3): + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + acorn: 8.12.1 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + mlly: 1.7.1 + pathe: 1.1.2 + pkg-types: 1.2.0 + scule: 1.3.0 + strip-literal: 2.1.0 + unplugin: 1.14.1 + transitivePeerDependencies: + - rollup + - webpack-sources + + universalify@2.0.1: {} + + unocss@0.62.4(postcss@8.4.47)(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)): + dependencies: + '@unocss/astro': 0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + '@unocss/cli': 0.62.4(rollup@4.21.3) + '@unocss/core': 0.62.4 + '@unocss/postcss': 0.62.4(postcss@8.4.47) + '@unocss/preset-attributify': 0.62.4 + '@unocss/preset-icons': 0.62.4 + '@unocss/preset-mini': 0.62.4 + '@unocss/preset-tagify': 0.62.4 + '@unocss/preset-typography': 0.62.4 + '@unocss/preset-uno': 0.62.4 + '@unocss/preset-web-fonts': 0.62.4 + '@unocss/preset-wind': 0.62.4 + '@unocss/transformer-attributify-jsx': 0.62.4 + '@unocss/transformer-compile-class': 0.62.4 + '@unocss/transformer-directives': 0.62.4 + '@unocss/transformer-variant-group': 0.62.4 + '@unocss/vite': 0.62.4(rollup@4.21.3)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)) + optionalDependencies: + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - postcss + - rollup + - supports-color + + unpipe@1.0.0: {} + + unplugin-auto-import@0.18.3(@vueuse/core@11.1.0(vue@3.5.6(typescript@5.6.2)))(rollup@4.21.3): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + minimatch: 9.0.5 + unimport: 3.12.0(rollup@4.21.3) + unplugin: 1.14.1 + optionalDependencies: + '@vueuse/core': 11.1.0(vue@3.5.6(typescript@5.6.2)) + transitivePeerDependencies: + - rollup + - webpack-sources + + unplugin-icons@0.19.3(@vue/compiler-sfc@3.5.6): + dependencies: + '@antfu/install-pkg': 0.4.1 + '@antfu/utils': 0.7.10 + '@iconify/utils': 2.1.33 + debug: 4.3.7 + kolorist: 1.8.0 + local-pkg: 0.5.0 + unplugin: 1.14.1 + optionalDependencies: + '@vue/compiler-sfc': 3.5.6 + transitivePeerDependencies: + - supports-color + - webpack-sources + + unplugin-vue-components@0.27.4(@babel/parser@7.25.6)(rollup@4.21.3)(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + chokidar: 3.6.0 + debug: 4.3.7 + fast-glob: 3.3.2 + local-pkg: 0.5.0 + magic-string: 0.30.11 + minimatch: 9.0.5 + mlly: 1.7.1 + unplugin: 1.14.1 + vue: 3.5.6(typescript@5.6.2) + optionalDependencies: + '@babel/parser': 7.25.6 + transitivePeerDependencies: + - rollup + - supports-color + - webpack-sources + + unplugin@1.14.1: + dependencies: + acorn: 8.12.1 + webpack-virtual-modules: 0.6.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + vdirs@0.1.8(vue@3.5.6(typescript@5.6.2)): + dependencies: + evtd: 0.2.4 + vue: 3.5.6(typescript@5.6.2) + + vite-plugin-compression@0.5.1(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)): + dependencies: + chalk: 4.1.2 + debug: 4.3.7 + fs-extra: 10.1.0 + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - supports-color + + vite-plugin-html@3.2.2(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)): + dependencies: + '@rollup/pluginutils': 4.2.1 + colorette: 2.0.20 + connect-history-api-fallback: 1.6.0 + consola: 2.15.3 + dotenv: 16.4.5 + dotenv-expand: 8.0.3 + ejs: 3.1.10 + fast-glob: 3.3.2 + fs-extra: 10.1.0 + html-minifier-terser: 6.1.0 + node-html-parser: 5.4.2 + pathe: 0.2.0 + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + + vite-plugin-mock@3.0.2(esbuild@0.23.1)(mockjs@1.1.0)(vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0)): + dependencies: + bundle-require: 4.2.1(esbuild@0.23.1) + chokidar: 3.6.0 + connect: 3.7.0 + debug: 4.3.7 + esbuild: 0.23.1 + fast-glob: 3.3.2 + mockjs: 1.1.0 + path-to-regexp: 6.3.0 + picocolors: 1.1.0 + vite: 5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0) + transitivePeerDependencies: + - supports-color + + vite@5.4.6(@types/node@20.16.5)(sass@1.78.0)(terser@5.32.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.47 + rollup: 4.21.3 + optionalDependencies: + '@types/node': 20.16.5 + fsevents: 2.3.3 + sass: 1.78.0 + terser: 5.32.0 + + vooks@0.2.12(vue@3.5.6(typescript@5.6.2)): + dependencies: + evtd: 0.2.4 + vue: 3.5.6(typescript@5.6.2) + + vscode-uri@3.0.8: {} + + vue-demi@0.13.11(vue@3.5.6(typescript@5.6.2)): + dependencies: + vue: 3.5.6(typescript@5.6.2) + + vue-demi@0.14.10(vue@3.5.6(typescript@5.6.2)): + dependencies: + vue: 3.5.6(typescript@5.6.2) + + vue-echarts@7.0.3(@vue/runtime-core@3.5.6)(echarts@5.5.1)(vue@3.5.6(typescript@5.6.2)): + dependencies: + echarts: 5.5.1 + vue: 3.5.6(typescript@5.6.2) + vue-demi: 0.13.11(vue@3.5.6(typescript@5.6.2)) + optionalDependencies: + '@vue/runtime-core': 3.5.6 + transitivePeerDependencies: + - '@vue/composition-api' + + vue-eslint-parser@9.4.3(eslint@8.57.0): + dependencies: + debug: 4.3.7 + eslint: 8.57.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + vue-i18n@10.0.1(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@intlify/core-base': 10.0.1 + '@intlify/shared': 10.0.1 + '@vue/devtools-api': 6.6.4 + vue: 3.5.6(typescript@5.6.2) + + vue-router@4.4.5(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.6(typescript@5.6.2) + + vue-tsc@2.1.6(typescript@5.6.2): + dependencies: + '@volar/typescript': 2.4.5 + '@vue/language-core': 2.1.6(typescript@5.6.2) + semver: 7.6.3 + typescript: 5.6.2 + + vue@3.5.6(typescript@5.6.2): + dependencies: + '@vue/compiler-dom': 3.5.6 + '@vue/compiler-sfc': 3.5.6 + '@vue/runtime-dom': 3.5.6 + '@vue/server-renderer': 3.5.6(vue@3.5.6(typescript@5.6.2)) + '@vue/shared': 3.5.6 + optionalDependencies: + typescript: 5.6.2 + + vueuc@0.4.58(vue@3.5.6(typescript@5.6.2)): + dependencies: + '@css-render/vue3-ssr': 0.15.14(vue@3.5.6(typescript@5.6.2)) + '@juggle/resize-observer': 3.4.0 + css-render: 0.15.14 + evtd: 0.2.4 + seemly: 0.3.8 + vdirs: 0.1.8(vue@3.5.6(typescript@5.6.2)) + vooks: 0.2.12(vue@3.5.6(typescript@5.6.2)) + vue: 3.5.6(typescript@5.6.2) + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xml-name-validator@4.0.0: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zrender@5.6.0: + dependencies: + tslib: 2.3.0 diff --git a/web/public/favicon.png b/web/public/favicon.png new file mode 100644 index 00000000..c3fa8f2f Binary files /dev/null and b/web/public/favicon.png differ diff --git a/web/public/loading/index.css b/web/public/loading/index.css new file mode 100644 index 00000000..4a887e81 --- /dev/null +++ b/web/public/loading/index.css @@ -0,0 +1,91 @@ +.loading-container { + position: fixed; + left: 0; + top: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} + +.loading-spin__container { + width: 56px; + height: 56px; + margin: 36px 0; +} + +.loading-spin { + position: relative; + height: 100%; + animation: loadingSpin 1s linear infinite; +} + +.left-0 { + left: 0; +} + +.right-0 { + right: 0; +} + +.top-0 { + top: 0; +} + +.bottom-0 { + bottom: 0; +} + +.loading-spin-item { + position: absolute; + height: 16px; + width: 16px; + background-color: var(--primary-color); + border-radius: 8px; + -webkit-animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + animation: loadingPulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes loadingSpin { + from { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes loadingPulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.loading-delay-500 { + -webkit-animation-delay: 500ms; + animation-delay: 500ms; +} + +.loading-delay-1000 { + -webkit-animation-delay: 1000ms; + animation-delay: 1000ms; +} + +.loading-delay-1500 { + -webkit-animation-delay: 1500ms; + animation-delay: 1500ms; +} + +.loading-title { + font-size: 28px; + font-weight: 500; + color: #6a6a6a; +} diff --git a/web/public/loading/index.js b/web/public/loading/index.js new file mode 100644 index 00000000..6c474ec1 --- /dev/null +++ b/web/public/loading/index.js @@ -0,0 +1,9 @@ +function addThemeColorCssVars() { + const key = '__THEME_COLOR__' + const defaultColor = '#66CCFF' + const themeColor = window.localStorage.getItem(key) || defaultColor + const cssVars = `--primary-color: ${themeColor}` + document.documentElement.style.cssText = cssVars +} + +addThemeColorCssVars() diff --git a/web/public/loading/logo.png b/web/public/loading/logo.png new file mode 100644 index 00000000..ab5e2126 Binary files /dev/null and b/web/public/loading/logo.png differ diff --git a/web/public/robots.txt b/web/public/robots.txt new file mode 100644 index 00000000..1f53798b --- /dev/null +++ b/web/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/web/settings/.gitignore b/web/settings/.gitignore new file mode 100644 index 00000000..de7b192a --- /dev/null +++ b/web/settings/.gitignore @@ -0,0 +1 @@ +proxy-config.ts \ No newline at end of file diff --git a/web/settings/proxy-config.ts.example b/web/settings/proxy-config.ts.example new file mode 100644 index 00000000..0924353b --- /dev/null +++ b/web/settings/proxy-config.ts.example @@ -0,0 +1,18 @@ +const proxyConfigMappings: Record = { + dev: { + prefix: '/api', + target: 'http://localhost:8080' + }, + test: { + prefix: '/api', + target: 'http://localhost:8080' + }, + prod: { + prefix: '/api', + target: 'http://localhost:8080' + } +} + +export function getProxyConfig(envType: ProxyType = 'dev'): ProxyConfig { + return proxyConfigMappings[envType] +} diff --git a/web/settings/theme.json b/web/settings/theme.json new file mode 100644 index 00000000..b4e1d049 --- /dev/null +++ b/web/settings/theme.json @@ -0,0 +1,25 @@ +{ + "isMobile": false, + "darkMode": false, + "sider": { + "width": 220, + "collapsedWidth": 64, + "collapsed": false + }, + "tab": { + "visible": true, + "height": 50 + }, + "header": { + "visible": true, + "height": 60 + }, + "primaryColor": "#66CCFF", + "otherColor": { + "info": "#2080F0", + "success": "#18A058", + "warning": "#F0A020", + "error": "#D03050" + }, + "language": "zh_CN" +} diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 00000000..a6fc6986 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,11 @@ + + + diff --git a/web/src/api/panel/cert/index.ts b/web/src/api/panel/cert/index.ts new file mode 100644 index 00000000..f6897b75 --- /dev/null +++ b/web/src/api/panel/cert/index.ts @@ -0,0 +1,59 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // CA 供应商列表 + caProviders: (): Promise> => request.get('/cert/caProviders'), + // DNS 供应商列表 + dnsProviders: (): Promise> => request.get('/cert/dnsProviders'), + // 证书算法列表 + algorithms: (): Promise> => request.get('/cert/algorithms'), + // ACME 用户列表 + users: (page: number, limit: number): Promise> => + request.get('/cert/users', { params: { page, limit } }), + // ACME 用户详情 + userInfo: (id: number): Promise> => request.get(`/cert/users/${id}`), + // ACME 用户添加 + userAdd: (data: any): Promise> => request.post('/cert/users', data), + // ACME 用户更新 + userUpdate: (id: number, data: any): Promise> => + request.put(`/cert/users/${id}`, data), + // ACME 用户删除 + userDelete: (id: number): Promise> => + request.delete(`/cert/users/${id}`), + // DNS 记录列表 + dns: (page: number, limit: number): Promise> => + request.get('/cert/dns', { params: { page, limit } }), + // DNS 记录详情 + dnsInfo: (id: number): Promise> => request.get(`/cert/dns/${id}`), + // DNS 记录添加 + dnsAdd: (data: any): Promise> => request.post('/cert/dns', data), + // DNS 记录更新 + dnsUpdate: (id: number, data: any): Promise> => + request.put(`/cert/dns/${id}`, data), + // DNS 记录删除 + dnsDelete: (id: number): Promise> => request.delete(`/cert/dns/${id}`), + // 证书列表 + certs: (page: number, limit: number): Promise> => + request.get('/cert/certs', { params: { page, limit } }), + // 证书详情 + certInfo: (id: number): Promise> => request.get(`/cert/certs/${id}`), + // 证书添加 + certAdd: (data: any): Promise> => request.post('/cert/certs', data), + // 证书更新 + certUpdate: (id: number, data: any): Promise> => + request.put(`/cert/certs/${id}`, data), + // 证书删除 + certDelete: (id: number): Promise> => + request.delete(`/cert/certs/${id}`), + // 签发 + obtain: (id: number): Promise> => request.post(`/cert/obtain`, { id }), + // 续签 + renew: (id: number): Promise> => request.post(`/cert/renew`, { id }), + // 获取 DNS 记录 + manualDNS: (id: number): Promise> => + request.post(`/cert/manualDNS`, { id }), + // 部署 + deploy: (id: number, website_id: number): Promise> => + request.post(`/cert/deploy`, { id, website_id }) +} diff --git a/web/src/api/panel/container/index.ts b/web/src/api/panel/container/index.ts new file mode 100644 index 00000000..eaf25972 --- /dev/null +++ b/web/src/api/panel/container/index.ts @@ -0,0 +1,106 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取容器列表 + containerList: (page: number, limit: number): Promise> => + request.get('/container/list', { params: { page, limit } }), + // 添加容器 + containerCreate: (config: any): Promise> => + request.post('/container/create', config), + // 删除容器 + containerRemove: (id: string): Promise> => + request.post(`/container/remove`, { id }), + // 启动容器 + containerStart: (id: string): Promise> => + request.post(`/container/start`, { id }), + // 停止容器 + containerStop: (id: string): Promise> => + request.post(`/container/stop`, { id }), + // 重启容器 + containerRestart: (id: string): Promise> => + request.post(`/container/restart`, { id }), + // 暂停容器 + containerPause: (id: string): Promise> => + request.post(`/container/pause`, { id }), + // 恢复容器 + containerUnpause: (id: string): Promise> => + request.post(`/container/unpause`, { id }), + // 获取容器详情 + containerDetail: (id: string): Promise> => + request.get(`/container/detail`, { params: { id } }), + // 杀死容器 + containerKill: (id: string): Promise> => + request.post(`/container/kill`, { id }), + // 重命名容器 + containerRename: (id: string, name: string): Promise> => + request.post(`/container/rename`, { id, name }), + // 获取容器状态 + containerStats: (id: string): Promise> => + request.get(`/container/stats`, { params: { id } }), + // 检查容器是否存在 + containerExist: (id: string): Promise> => + request.get(`/container/exist`, { params: { id } }), + // 获取容器日志 + containerLogs: (id: string): Promise> => + request.get(`/container/logs`, { params: { id } }), + // 清理容器 + containerPrune: (): Promise> => request.post(`/container/prune`), + // 获取网络列表 + networkList: (page: number, limit: number): Promise> => + request.get(`/container/network/list`, { params: { page, limit } }), + // 创建网络 + networkCreate: (config: any): Promise> => + request.post(`/container/network/create`, config), + // 删除网络 + networkRemove: (id: string): Promise> => + request.post(`/container/network/remove`, { id }), + // 检查网络是否存在 + networkExist: (id: string): Promise> => + request.get(`/container/network/exist`, { params: { id } }), + // 获取网络详情 + networkInspect: (id: string): Promise> => + request.get(`/container/network/inspect`, { params: { id } }), + // 连接容器到网络 + networkConnect: (config: any): Promise> => + request.post(`/container/network/connect`, config), + // 断开容器到网络的连接 + networkDisconnect: (config: any): Promise> => + request.post(`/container/network/disconnect`, config), + // 清理网络 + networkPrune: (): Promise> => request.post(`/container/network/prune`), + // 获取镜像列表 + imageList: (page: number, limit: number): Promise> => + request.get(`/container/image/list`, { params: { page, limit } }), + // 检查镜像是否存在 + imageExist: (id: string): Promise> => + request.get(`/container/image/exist`, { params: { id } }), + // 拉取镜像 + imagePull: (config: any): Promise> => + request.post(`/container/image/pull`, config), + // 删除镜像 + imageRemove: (id: string): Promise> => + request.post(`/container/image/remove`, { id }), + // 获取镜像详情 + imageInspect: (id: string): Promise> => + request.get(`/container/image/inspect`, { params: { id } }), + // 清理镜像 + imagePrune: (): Promise> => request.post(`/container/image/prune`), + // 获取卷列表 + volumeList: (page: number, limit: number): Promise> => + request.get(`/container/volume/list`, { params: { page, limit } }), + // 创建卷 + volumeCreate: (config: any): Promise> => + request.post(`/container/volume/create`, config), + // 检查卷是否存在 + volumeExist: (id: string): Promise> => + request.get(`/container/volume/exist`, { params: { id } }), + // 删除卷 + volumeRemove: (id: string): Promise> => + request.post(`/container/volume/remove`, { id }), + // 获取卷详情 + volumeInspect: (id: string): Promise> => + request.get(`/container/volume/inspect`, { params: { id } }), + // 清理卷 + volumePrune: (): Promise> => request.post(`/container/volume/prune`) +} diff --git a/web/src/api/panel/cron/index.ts b/web/src/api/panel/cron/index.ts new file mode 100644 index 00000000..110953cd --- /dev/null +++ b/web/src/api/panel/cron/index.ts @@ -0,0 +1,22 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取任务列表 + list: (page: number, limit: number): Promise> => + request.get('/cron/list', { params: { page, limit } }), + // 获取任务脚本 + script: (id: number): Promise> => request.get('/cron/' + id), + // 添加任务 + add: (task: any): Promise> => request.post('/cron/add', task), + // 修改任务 + update: (id: number, name: string, time: string, script: string): Promise> => + request.put('/cron/' + id, { name, time, script }), + // 删除任务 + delete: (id: number): Promise> => request.delete('/cron/' + id), + // 获取任务日志 + log: (id: number): Promise> => request.get('/cron/log/' + id), + // 修改任务状态 + status: (id: number, status: boolean): Promise> => + request.post('/cron/status', { id, status }) +} diff --git a/web/src/api/panel/file/index.ts b/web/src/api/panel/file/index.ts new file mode 100644 index 00000000..431b9a23 --- /dev/null +++ b/web/src/api/panel/file/index.ts @@ -0,0 +1,59 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 创建文件/文件夹 + create: (path: string, dir: boolean): Promise> => + request.post('/file/create', { path, dir }), + // 获取文件内容 + content: (path: string): Promise> => + request.get('/file/content', { params: { path } }), + // 保存文件 + save: (path: string, content: string): Promise> => + request.post('/file/save', { path, content }), + // 删除文件 + delete: (path: string): Promise> => + request.post('/file/delete', { path }), + // 上传文件 + upload: (path: string, formData: FormData, onProgress: any): Promise> => { + formData.append('path', path) + return request.post('/file/upload', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + onUploadProgress: (progressEvent: any) => { + onProgress({ percent: Math.ceil((progressEvent.loaded / progressEvent.total) * 100) }) + } + }) + }, + // 移动文件 + move: (source: string, target: string): Promise> => + request.post('/file/move', { source, target }), + // 复制文件 + copy: (source: string, target: string): Promise> => + request.post('/file/copy', { source, target }), + // 远程下载 + remoteDownload: (url: string, path: string): Promise> => + request.post('/file/remoteDownload', { url, path }), + // 获取文件信息 + info: (path: string): Promise> => + request.get('/file/info', { params: { path } }), + // 修改文件权限 + permission: ( + path: string, + mode: string, + owner: string, + group: string + ): Promise> => + request.post('/file/permission', { path, mode, owner, group }), + // 压缩文件 + archive: (paths: string[], file: string): Promise> => + request.post('/file/archive', { paths, file }), + // 解压文件 + unArchive: (file: string, path: string): Promise> => + request.post('/file/unArchive', { file, path }), + // 搜索文件 + search: (keyword: string): Promise> => + request.post('/file/search', { keyword }), + // 获取文件列表 + list: (path: string, page: number, limit: number): Promise> => + request.get('/file/list', { params: { path, page, limit } }) +} diff --git a/web/src/api/panel/info/index.ts b/web/src/api/panel/info/index.ts new file mode 100644 index 00000000..acf1e9bb --- /dev/null +++ b/web/src/api/panel/info/index.ts @@ -0,0 +1,28 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 面板信息 + panel: (): Promise => fetch('/api/info/panel'), + // 面板菜单 + menu: (): Promise> => request.get('/info/menu'), + // 首页插件 + homePlugins: (): Promise> => request.get('/info/homePlugins'), + // 实时监控 + nowMonitor: (): Promise> => request.get('/info/nowMonitor'), + // 系统信息 + systemInfo: (): Promise> => request.get('/info/systemInfo'), + // 统计信息 + countInfo: (): Promise> => request.get('/info/countInfo'), + // 已安装的数据库和PHP + installedDbAndPhp: (): Promise> => + request.get('/info/installedDbAndPhp'), + // 检查更新 + checkUpdate: (): Promise> => request.get('/info/checkUpdate'), + // 更新日志 + updateInfo: (): Promise> => request.get('/info/updateInfo'), + // 更新面板 + update: (): Promise> => request.post('/info/update', null), + // 重启面板 + restart: (): Promise> => request.post('/info/restart') +} diff --git a/web/src/api/panel/monitor/index.ts b/web/src/api/panel/monitor/index.ts new file mode 100644 index 00000000..e578bc5d --- /dev/null +++ b/web/src/api/panel/monitor/index.ts @@ -0,0 +1,18 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 开关 + switch: (monitor: boolean): Promise> => + request.post('/monitor/switch', { monitor }), + // 保存天数 + saveDays: (days: number): Promise> => + request.post('/monitor/saveDays', { days }), + // 清空监控记录 + clear: (): Promise> => request.post('/monitor/clear'), + // 监控记录 + list: (start: number, end: number): Promise> => + request.get('/monitor/list', { params: { start, end } }), + // 开关和天数 + switchAndDays: (): Promise> => request.get('/monitor/switchAndDays') +} diff --git a/web/src/api/panel/plugin/index.ts b/web/src/api/panel/plugin/index.ts new file mode 100644 index 00000000..59ea5823 --- /dev/null +++ b/web/src/api/panel/plugin/index.ts @@ -0,0 +1,23 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取插件列表 + list: (page: number, limit: number): Promise> => + request.get('/plugin/list', { params: { page, limit } }), + // 安装插件 + install: (slug: string): Promise> => + request.post('/plugin/install', { slug }), + // 卸载插件 + uninstall: (slug: string): Promise> => + request.post('/plugin/uninstall', { slug }), + // 更新插件 + update: (slug: string): Promise> => + request.post('/plugin/update', { slug }), + // 设置首页显示 + updateShow: (slug: string, show: boolean): Promise> => + request.post('/plugin/updateShow', { slug, show }), + // 插件是否已安装 + isInstalled: (slug: string): Promise> => + request.get('/plugin/isInstalled', { params: { slug } }) +} diff --git a/web/src/api/panel/safe/index.ts b/web/src/api/panel/safe/index.ts new file mode 100644 index 00000000..df11e7cf --- /dev/null +++ b/web/src/api/panel/safe/index.ts @@ -0,0 +1,34 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取防火墙状态 + firewallStatus: (): Promise> => request.get('/safe/firewallStatus'), + // 设置防火墙状态 + setFirewallStatus: (status: boolean): Promise> => + request.post('/safe/firewallStatus', { status }), + // 获取防火墙规则 + firewallRules: (page: number, limit: number): Promise> => + request.get('/safe/firewallRules', { params: { page, limit } }), + // 添加防火墙规则 + addFirewallRule: (port: string, protocol: string): Promise> => + request.post('/safe/firewallRules', { port, protocol }), + // 删除防火墙规则 + deleteFirewallRule: (port: string, protocol: string): Promise> => + request.delete('/safe/firewallRules', { data: { port, protocol } }), + // 获取SSH状态 + sshStatus: (): Promise> => request.get('/safe/sshStatus'), + // 设置SSH状态 + setSshStatus: (status: boolean): Promise> => + request.post('/safe/sshStatus', { status }), + // 获取SSH端口 + sshPort: (): Promise> => request.get('/safe/sshPort'), + // 设置SSH端口 + setSshPort: (port: number): Promise> => + request.post('/safe/sshPort', { port }), + // 获取Ping状态 + pingStatus: (): Promise> => request.get('/safe/pingStatus'), + // 设置Ping状态 + setPingStatus: (status: boolean): Promise> => + request.post('/safe/pingStatus', { status }) +} diff --git a/web/src/api/panel/setting/index.ts b/web/src/api/panel/setting/index.ts new file mode 100644 index 00000000..1384ec4f --- /dev/null +++ b/web/src/api/panel/setting/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取设置 + list: (): Promise> => request.get('/setting/list'), + // 保存设置 + update: (settings: any): Promise> => + request.post('/setting/update', settings), + // 获取HTTPS设置 + getHttps: (): Promise> => request.get('/setting/https'), + // 保存HTTPS设置 + updateHttps: (https: any): Promise> => + request.post('/setting/https', https) +} diff --git a/web/src/api/panel/ssh/index.ts b/web/src/api/panel/ssh/index.ts new file mode 100644 index 00000000..642c7335 --- /dev/null +++ b/web/src/api/panel/ssh/index.ts @@ -0,0 +1,14 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取信息 + info: (): Promise> => request.get('/ssh/info'), + // 保存信息 + saveInfo: ( + host: string, + port: number, + user: string, + password: string + ): Promise> => request.post('/ssh/info', { host, port, user, password }) +} diff --git a/web/src/api/panel/system/service/index.ts b/web/src/api/panel/system/service/index.ts new file mode 100644 index 00000000..343b2416 --- /dev/null +++ b/web/src/api/panel/system/service/index.ts @@ -0,0 +1,29 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 服务状态 + status: (service: string): Promise> => + request.get('/system/service/status', { params: { service } }), + // 是否启用服务 + isEnabled: (service: string): Promise> => + request.get('/system/service/isEnabled', { params: { service } }), + // 启用服务 + enable: (service: string): Promise> => + request.post('/system/service/enable', { service }), + // 禁用服务 + disable: (service: string): Promise> => + request.post('/system/service/disable', { service }), + // 重启服务 + restart: (service: string): Promise> => + request.post('/system/service/restart', { service }), + // 重载服务 + reload: (service: string): Promise> => + request.post('/system/service/reload', { service }), + // 启动服务 + start: (service: string): Promise> => + request.post('/system/service/start', { service }), + // 停止服务 + stop: (service: string): Promise> => + request.post('/system/service/stop', { service }) +} diff --git a/web/src/api/panel/task/index.ts b/web/src/api/panel/task/index.ts new file mode 100644 index 00000000..1af7d840 --- /dev/null +++ b/web/src/api/panel/task/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取状态 + status: (): Promise> => request.get('/task/status'), + // 获取任务列表 + list: (page: number, limit: number): Promise> => + request.get('/task/list', { params: { page, limit } }), + // 获取任务日志 + log: (id: number): Promise> => + request.get('/task/log', { params: { id } }), + // 删除任务 + delete: (id: number): Promise> => request.post('/task/delete', { id }) +} diff --git a/web/src/api/panel/user/index.ts b/web/src/api/panel/user/index.ts new file mode 100644 index 00000000..a0be3c30 --- /dev/null +++ b/web/src/api/panel/user/index.ts @@ -0,0 +1,17 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 登录 + login: (username: string, password: string): Promise> => + request.post('/user/login', { + username, + password + }), + // 登出 + logout: (): Promise> => request.post('/user/logout'), + // 是否登录 + isLogin: (): Promise> => request.get('/user/isLogin'), + // 获取用户信息 + info: (): Promise> => request.get('/user/info') +} diff --git a/web/src/api/panel/website/index.ts b/web/src/api/panel/website/index.ts new file mode 100644 index 00000000..1c8dca05 --- /dev/null +++ b/web/src/api/panel/website/index.ts @@ -0,0 +1,50 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 列表 + list: (page: number, limit: number): Promise> => + request.get('/websites', { params: { page, limit } }), + // 添加 + add: (data: any): Promise> => request.post('/websites', data), + // 删除 + delete: (data: any): Promise> => request.post('/websites/delete', data), + // 获取默认配置 + defaultConfig: (): Promise> => request.get('/website/defaultConfig'), + // 保存默认配置 + saveDefaultConfig: (index: string, stop: string): Promise> => + request.post('/website/defaultConfig', { index, stop }), + // 网站配置 + config: (id: number): Promise> => + request.get('/websites/' + id + '/config'), + // 保存网站配置 + saveConfig: (id: number, data: any): Promise> => + request.post('/websites/' + id + '/config', data), + // 清空日志 + clearLog: (id: number): Promise> => + request.delete('/websites/' + id + '/log'), + // 更新备注 + updateRemark: (id: number, remark: string): Promise> => + request.post('/websites/' + id + '/updateRemark', { remark }), + // 获取备份列表 + backupList: (page: number, limit: number): Promise> => + request.get('/website/backupList', { params: { page, limit } }), + // 创建备份 + createBackup: (id: number): Promise> => + request.post('/websites/' + id + '/createBackup', {}), + // 上传备份 + uploadBackup: (data: any): Promise> => + request.put('/website/uploadBackup', data), + // 删除备份 + deleteBackup: (name: string): Promise> => + request.delete('/website/deleteBackup', { data: { name } }), + // 恢复备份 + restoreBackup: (id: number, name: number): Promise> => + request.post('/websites/' + id + '/restoreBackup', { name }), + // 重置配置 + resetConfig: (id: number): Promise> => + request.post('/websites/' + id + '/resetConfig'), + // 修改状态 + status: (id: number, status: boolean): Promise> => + request.post('/websites/' + id + '/status', { status }) +} diff --git a/web/src/api/plugins/fail2ban/index.ts b/web/src/api/plugins/fail2ban/index.ts new file mode 100644 index 00000000..3bf8e67e --- /dev/null +++ b/web/src/api/plugins/fail2ban/index.ts @@ -0,0 +1,24 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 保护列表 + jails: (page: number, limit: number): Promise> => + request.get('/plugins/fail2ban/jails', { params: { page, limit } }), + // 添加保护 + add: (data: any): Promise> => request.post('/plugins/fail2ban/jails', data), + // 删除保护 + delete: (name: string): Promise> => + request.delete('/plugins/fail2ban/jails', { params: { name } }), + // 封禁列表 + jail: (name: string): Promise> => + request.get('/plugins/fail2ban/jails/' + name), + // 解封 IP + unban: (name: string, ip: string): Promise> => + request.post('/plugins/fail2ban/unban', { name, ip }), + // 获取白名单 + whitelist: (): Promise> => request.get('/plugins/fail2ban/whiteList'), + // 设置白名单 + setWhitelist: (ip: string): Promise> => + request.post('/plugins/fail2ban/whiteList', { ip }) +} diff --git a/web/src/api/plugins/frp/index.ts b/web/src/api/plugins/frp/index.ts new file mode 100644 index 00000000..dfe43919 --- /dev/null +++ b/web/src/api/plugins/frp/index.ts @@ -0,0 +1,11 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取配置 + config: (service: string): Promise> => + request.get('/plugins/frp/config', { params: { service } }), + // 保存配置 + saveConfig: (service: string, config: string): Promise> => + request.post('/plugins/frp/config', { service, config }) +} diff --git a/web/src/api/plugins/gitea/index.ts b/web/src/api/plugins/gitea/index.ts new file mode 100644 index 00000000..0b7005d9 --- /dev/null +++ b/web/src/api/plugins/gitea/index.ts @@ -0,0 +1,10 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取配置 + config: (): Promise> => request.get('/plugins/gitea/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/gitea/config', { config }) +} diff --git a/web/src/api/plugins/mysql/index.ts b/web/src/api/plugins/mysql/index.ts new file mode 100644 index 00000000..81622432 --- /dev/null +++ b/web/src/api/plugins/mysql/index.ts @@ -0,0 +1,63 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (): Promise> => request.get('/plugins/mysql/load'), + // 获取配置 + config: (): Promise> => request.get('/plugins/mysql/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/mysql/config', { config }), + // 获取错误日志 + errorLog: (): Promise> => request.get('/plugins/mysql/errorLog'), + // 清空错误日志 + clearErrorLog: (): Promise> => request.post('/plugins/mysql/clearErrorLog'), + // 获取慢查询日志 + slowLog: (): Promise> => request.get('/plugins/mysql/slowLog'), + // 清空慢查询日志 + clearSlowLog: (): Promise> => request.post('/plugins/mysql/clearSlowLog'), + // 获取 root 密码 + rootPassword: (): Promise> => request.get('/plugins/mysql/rootPassword'), + // 修改 root 密码 + setRootPassword: (password: string): Promise> => + request.post('/plugins/mysql/rootPassword', { password }), + // 数据库列表 + databases: (page: number, limit: number): Promise> => + request.get('/plugins/mysql/databases', { params: { page, limit } }), + // 创建数据库 + addDatabase: (database: any): Promise> => + request.post('/plugins/mysql/databases', database), + // 删除数据库 + deleteDatabase: (database: string): Promise> => + request.delete('/plugins/mysql/databases', { params: { database } }), + // 备份列表 + backups: (page: number, limit: number): Promise> => + request.get('/plugins/mysql/backups', { params: { page, limit } }), + // 创建备份 + createBackup: (database: string): Promise> => + request.post('/plugins/mysql/backups', { database }), + // 上传备份 + uploadBackup: (backup: any): Promise> => + request.put('/plugins/mysql/backups', backup), + // 删除备份 + deleteBackup: (name: string): Promise> => + request.delete('/plugins/mysql/backups', { params: { name } }), + // 还原备份 + restoreBackup: (backup: string, database: string): Promise> => + request.post('/plugins/mysql/backups/restore', { backup, database }), + // 用户列表 + users: (page: number, limit: number): Promise> => + request.get('/plugins/mysql/users', { params: { page, limit } }), + // 创建用户 + addUser: (user: any): Promise> => request.post('/plugins/mysql/users', user), + // 删除用户 + deleteUser: (user: string): Promise> => + request.delete('/plugins/mysql/users', { params: { user } }), + // 设置用户密码 + setUserPassword: (user: string, password: string): Promise> => + request.post('/plugins/mysql/users/password', { user, password }), + // 设置用户权限 + setUserPrivileges: (user: string, database: string): Promise> => + request.post('/plugins/mysql/users/privileges', { user, database }) +} diff --git a/web/src/api/plugins/openresty/index.ts b/web/src/api/plugins/openresty/index.ts new file mode 100644 index 00000000..5915a04a --- /dev/null +++ b/web/src/api/plugins/openresty/index.ts @@ -0,0 +1,16 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (): Promise> => request.get('/plugins/openresty/load'), + // 获取配置 + config: (): Promise> => request.get('/plugins/openresty/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/openresty/config', { config }), + // 获取错误日志 + errorLog: (): Promise> => request.get('/plugins/openresty/errorLog'), + // 清空错误日志 + clearErrorLog: (): Promise> => request.post('/plugins/openresty/clearErrorLog') +} diff --git a/web/src/api/plugins/php/index.ts b/web/src/api/plugins/php/index.ts new file mode 100644 index 00000000..c56578e0 --- /dev/null +++ b/web/src/api/plugins/php/index.ts @@ -0,0 +1,41 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (version: number): Promise> => + request.get(`/plugins/php/${version}/load`), + // 获取配置 + config: (version: number): Promise> => + request.get(`/plugins/php/${version}/config`), + // 保存配置 + saveConfig: (version: number, config: string): Promise> => + request.post(`/plugins/php/${version}/config`, { config }), + // 获取FPM配置 + fpmConfig: (version: number): Promise> => + request.get(`/plugins/php/${version}/fpmConfig`), + // 保存FPM配置 + saveFPMConfig: (version: number, config: string): Promise> => + request.post(`/plugins/php/${version}/fpmConfig`, { config }), + // 获取错误日志 + errorLog: (version: number): Promise> => + request.get(`/plugins/php/${version}/errorLog`), + // 清空错误日志 + clearErrorLog: (version: number): Promise> => + request.post(`/plugins/php/${version}/clearErrorLog`), + // 获取慢日志 + slowLog: (version: number): Promise> => + request.get(`/plugins/php/${version}/slowLog`), + // 清空慢日志 + clearSlowLog: (version: number): Promise> => + request.post(`/plugins/php/${version}/clearSlowLog`), + // 拓展列表 + extensions: (version: number): Promise> => + request.get(`/plugins/php/${version}/extensions`), + // 安装拓展 + installExtension: (version: number, slug: string): Promise> => + request.post(`/plugins/php/${version}/extensions`, { slug }), + // 卸载拓展 + uninstallExtension: (version: number, slug: string): Promise> => + request.delete(`/plugins/php/${version}/extensions`, { params: { slug } }) +} diff --git a/web/src/api/plugins/phpmyadmin/index.ts b/web/src/api/plugins/phpmyadmin/index.ts new file mode 100644 index 00000000..5df266d3 --- /dev/null +++ b/web/src/api/plugins/phpmyadmin/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取信息 + info: (): Promise> => request.get('/plugins/phpmyadmin/info'), + // 设置端口 + port: (port: number): Promise> => + request.post('/plugins/phpmyadmin/port', { port }), + // 获取配置 + getConfig: (): Promise> => request.get('/plugins/phpmyadmin/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/phpmyadmin/config', { config }) +} diff --git a/web/src/api/plugins/podman/index.ts b/web/src/api/plugins/podman/index.ts new file mode 100644 index 00000000..931958bb --- /dev/null +++ b/web/src/api/plugins/podman/index.ts @@ -0,0 +1,15 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取注册表配置 + registryConfig: (): Promise> => request.get('/plugins/podman/registryConfig'), + // 保存注册表配置 + saveRegistryConfig: (config: string): Promise> => + request.post('/plugins/podman/registryConfig', { config }), + // 获取存储配置 + storageConfig: (): Promise> => request.get('/plugins/podman/storageConfig'), + // 保存存储配置 + saveStorageConfig: (config: string): Promise> => + request.post('/plugins/podman/storageConfig', { config }) +} diff --git a/web/src/api/plugins/postgresql/index.ts b/web/src/api/plugins/postgresql/index.ts new file mode 100644 index 00000000..686ecf39 --- /dev/null +++ b/web/src/api/plugins/postgresql/index.ts @@ -0,0 +1,57 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (): Promise> => request.get('/plugins/postgresql/load'), + // 获取配置 + config: (): Promise> => request.get('/plugins/postgresql/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/postgresql/config', { config }), + // 获取用户配置 + userConfig: (): Promise> => request.get('/plugins/postgresql/userConfig'), + // 保存配置 + saveUserConfig: (config: string): Promise> => + request.post('/plugins/postgresql/userConfig', { config }), + // 获取日志 + log: (): Promise> => request.get('/plugins/postgresql/log'), + // 清空错误日志 + clearLog: (): Promise> => request.post('/plugins/postgresql/clearLog'), + // 数据库列表 + databases: (page: number, limit: number): Promise> => + request.get('/plugins/postgresql/databases', { params: { page, limit } }), + // 创建数据库 + addDatabase: (database: any): Promise> => + request.post('/plugins/postgresql/databases', database), + // 删除数据库 + deleteDatabase: (database: string): Promise> => + request.delete('/plugins/postgresql/databases', { params: { database } }), + // 备份列表 + backups: (page: number, limit: number): Promise> => + request.get('/plugins/postgresql/backups', { params: { page, limit } }), + // 创建备份 + createBackup: (database: string): Promise> => + request.post('/plugins/postgresql/backups', { database }), + // 上传备份 + uploadBackup: (backup: any): Promise> => + request.put('/plugins/postgresql/backups', backup), + // 删除备份 + deleteBackup: (name: string): Promise> => + request.delete('/plugins/postgresql/backups', { params: { name } }), + // 还原备份 + restoreBackup: (backup: string, database: string): Promise> => + request.post('/plugins/postgresql/backups/restore', { backup, database }), + // 角色列表 + roles: (page: number, limit: number): Promise> => + request.get('/plugins/postgresql/roles', { params: { page, limit } }), + // 创建角色 + addRole: (user: any): Promise> => + request.post('/plugins/postgresql/roles', user), + // 删除角色 + deleteRole: (user: string): Promise> => + request.delete('/plugins/postgresql/roles', { params: { user } }), + // 设置角色密码 + setRolePassword: (user: string, password: string): Promise> => + request.post('/plugins/postgresql/roles/password', { user, password }) +} diff --git a/web/src/api/plugins/pureftpd/index.ts b/web/src/api/plugins/pureftpd/index.ts new file mode 100644 index 00000000..79bad008 --- /dev/null +++ b/web/src/api/plugins/pureftpd/index.ts @@ -0,0 +1,22 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 列表 + list: (page: number, limit: number): Promise> => + request.get('/plugins/pureftpd/list', { params: { page, limit } }), + // 添加 + add: (username: string, password: string, path: string): Promise> => + request.post('/plugins/pureftpd/add', { username, password, path }), + // 删除 + delete: (username: string): Promise> => + request.delete('/plugins/pureftpd/delete', { params: { username } }), + // 修改密码 + changePassword: (username: string, password: string): Promise> => + request.post('/plugins/pureftpd/changePassword', { username, password }), + // 获取端口 + port: (): Promise> => request.get('/plugins/pureftpd/port'), + // 修改端口 + setPort: (port: number): Promise> => + request.post('/plugins/pureftpd/port', { port }) +} diff --git a/web/src/api/plugins/redis/index.ts b/web/src/api/plugins/redis/index.ts new file mode 100644 index 00000000..8e3d6324 --- /dev/null +++ b/web/src/api/plugins/redis/index.ts @@ -0,0 +1,12 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 负载状态 + load: (): Promise> => request.get('/plugins/redis/load'), + // 获取配置 + config: (): Promise> => request.get('/plugins/redis/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/redis/config', { config }) +} diff --git a/web/src/api/plugins/rsync/index.ts b/web/src/api/plugins/rsync/index.ts new file mode 100644 index 00000000..22708131 --- /dev/null +++ b/web/src/api/plugins/rsync/index.ts @@ -0,0 +1,22 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 获取配置 + config: (): Promise> => request.get('/plugins/rsync/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/rsync/config', { config }), + // 模块列表 + modules: (page: number, limit: number): Promise> => + request.get('/plugins/rsync/modules', { params: { page, limit } }), + // 添加模块 + addModule: (module: any): Promise> => + request.post('/plugins/rsync/modules', module), + // 删除模块 + deleteModule: (name: string): Promise> => + request.delete('/plugins/rsync/modules/' + name), + // 更新模块 + updateModule: (name: string, module: any): Promise> => + request.post('/plugins/rsync/modules/' + name, module) +} diff --git a/web/src/api/plugins/s3fs/index.ts b/web/src/api/plugins/s3fs/index.ts new file mode 100644 index 00000000..c8c912e8 --- /dev/null +++ b/web/src/api/plugins/s3fs/index.ts @@ -0,0 +1,12 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 列表 + list: (page: number, limit: number): Promise> => + request.get('/plugins/s3fs/list', { params: { page, limit } }), + // 添加 + add: (data: any): Promise> => request.post('/plugins/s3fs/add', data), + // 删除 + delete: (id: number): Promise> => request.post('/plugins/s3fs/delete', { id }) +} diff --git a/web/src/api/plugins/supervisor/index.ts b/web/src/api/plugins/supervisor/index.ts new file mode 100644 index 00000000..29da895b --- /dev/null +++ b/web/src/api/plugins/supervisor/index.ts @@ -0,0 +1,48 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // 服务名称 + service: (): Promise> => request.get('/plugins/supervisor/service'), + // 负载状态 + load: (): Promise> => request.get('/plugins/supervisor/load'), + // 获取错误日志 + log: (): Promise> => request.get('/plugins/supervisor/log'), + // 清空错误日志 + clearLog: (): Promise> => request.post('/plugins/supervisor/clearLog'), + // 获取配置 + config: (): Promise> => request.get('/plugins/supervisor/config'), + // 保存配置 + saveConfig: (config: string): Promise> => + request.post('/plugins/supervisor/config', { config }), + // 进程列表 + processes: (page: number, limit: number): Promise> => + request.get('/plugins/supervisor/processes', { params: { page, limit } }), + // 进程启动 + startProcess: (process: string): Promise> => + request.post('/plugins/supervisor/startProcess', { process }), + // 进程停止 + stopProcess: (process: string): Promise> => + request.post('/plugins/supervisor/stopProcess', { process }), + // 进程重启 + restartProcess: (process: string): Promise> => + request.post('/plugins/supervisor/restartProcess', { process }), + // 进程日志 + processLog: (process: string): Promise> => + request.get('/plugins/supervisor/processLog', { params: { process } }), + // 清空进程日志 + clearProcessLog: (process: string): Promise> => + request.post('/plugins/supervisor/clearProcessLog', { process }), + // 进程配置 + processConfig: (process: string): Promise> => + request.get('/plugins/supervisor/processConfig', { params: { process } }), + // 保存进程配置 + saveProcessConfig: (process: string, config: string): Promise> => + request.post('/plugins/supervisor/processConfig', { process, config }), + // 添加进程 + addProcess: (process: any): Promise> => + request.post('/plugins/supervisor/addProcess', process), + // 删除进程 + deleteProcess: (process: string): Promise> => + request.post('/plugins/supervisor/deleteProcess', { process }) +} diff --git a/web/src/api/plugins/toolbox/index.ts b/web/src/api/plugins/toolbox/index.ts new file mode 100644 index 00000000..51939554 --- /dev/null +++ b/web/src/api/plugins/toolbox/index.ts @@ -0,0 +1,28 @@ +import { request } from '@/utils' +import type { AxiosResponse } from 'axios' + +export default { + // DNS + dns: (): Promise> => request.get('/plugins/toolbox/dns'), + // 设置 DNS + setDns: (dns1: string, dns2: string): Promise> => + request.post('/plugins/toolbox/dns', { dns1, dns2 }), + // SWAP + swap: (): Promise> => request.get('/plugins/toolbox/swap'), + // 设置 SWAP + setSwap: (size: number): Promise> => + request.post('/plugins/toolbox/swap', { size }), + // 时区 + timezone: (): Promise> => request.get('/plugins/toolbox/timezone'), + // 设置时区 + setTimezone: (timezone: string): Promise> => + request.post('/plugins/toolbox/timezone', { timezone }), + // Hosts + hosts: (): Promise> => request.get('/plugins/toolbox/hosts'), + // 设置 Hosts + setHosts: (hosts: string): Promise> => + request.post('/plugins/toolbox/hosts', { hosts }), + // 设置 Root 密码 + setRootPassword: (password: string): Promise> => + request.post('/plugins/toolbox/rootPassword', { password }) +} diff --git a/web/src/assets/images/404.webp b/web/src/assets/images/404.webp new file mode 100644 index 00000000..3482bf96 Binary files /dev/null and b/web/src/assets/images/404.webp differ diff --git a/web/src/assets/images/login_banner.png b/web/src/assets/images/login_banner.png new file mode 100644 index 00000000..cd29ced4 Binary files /dev/null and b/web/src/assets/images/login_banner.png differ diff --git a/web/src/assets/images/login_bg.webp b/web/src/assets/images/login_bg.webp new file mode 100644 index 00000000..2a9df19d Binary files /dev/null and b/web/src/assets/images/login_bg.webp differ diff --git a/web/src/assets/images/logo.png b/web/src/assets/images/logo.png new file mode 100644 index 00000000..f89808dd Binary files /dev/null and b/web/src/assets/images/logo.png differ diff --git a/web/src/components/common/AppFooter.vue b/web/src/components/common/AppFooter.vue new file mode 100644 index 00000000..0b9e1ef0 --- /dev/null +++ b/web/src/components/common/AppFooter.vue @@ -0,0 +1,48 @@ + + + diff --git a/web/src/components/common/AppProvider.vue b/web/src/components/common/AppProvider.vue new file mode 100644 index 00000000..3212e222 --- /dev/null +++ b/web/src/components/common/AppProvider.vue @@ -0,0 +1,59 @@ + + + diff --git a/web/src/components/common/CodeEditor.vue b/web/src/components/common/CodeEditor.vue new file mode 100644 index 00000000..987a1081 --- /dev/null +++ b/web/src/components/common/CodeEditor.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/web/src/components/common/CronSelect.vue b/web/src/components/common/CronSelect.vue new file mode 100644 index 00000000..c041a84e --- /dev/null +++ b/web/src/components/common/CronSelect.vue @@ -0,0 +1,417 @@ + + + diff --git a/web/src/components/custom/TheIcon.vue b/web/src/components/custom/TheIcon.vue new file mode 100644 index 00000000..63b82e7c --- /dev/null +++ b/web/src/components/custom/TheIcon.vue @@ -0,0 +1,18 @@ + + + diff --git a/web/src/components/page/AppPage.vue b/web/src/components/page/AppPage.vue new file mode 100644 index 00000000..f570ba94 --- /dev/null +++ b/web/src/components/page/AppPage.vue @@ -0,0 +1,18 @@ + + + diff --git a/web/src/components/page/CommonPage.vue b/web/src/components/page/CommonPage.vue new file mode 100644 index 00000000..06a6e145 --- /dev/null +++ b/web/src/components/page/CommonPage.vue @@ -0,0 +1,40 @@ + + + diff --git a/web/src/i18n/en.json b/web/src/i18n/en.json new file mode 100644 index 00000000..e41fe5d1 --- /dev/null +++ b/web/src/i18n/en.json @@ -0,0 +1,402 @@ +{ + "name": "Rat Panel", + "certIndex": { + "title": "Certificates" + }, + "containerIndex": { + "title": "Containers" + }, + "cronIndex": { + "title": "Crond" + }, + "fileIndex": { + "title": "Files" + }, + "homeIndex": { + "title": "Dashboard", + "website": "Website", + "database": "Database", + "cron": "Scheduled Tasks", + "sponsor": "Sponsor", + "git": "Open Source", + "resources": { + "title": "Resource Usage", + "cpu": { + "used": "{used} CPU / {total} threads" + }, + "memory": { + "title": "Memory", + "physical": { + "used": "Physical used {used} / {total} total" + }, + "swap": { + "used": "Swap used {used} / {total} total" + } + } + }, + "loads": { + "title": "System Load", + "load": "{load} minute load", + "time": "nearly {time} minutes" + }, + "traffic": { + "title": "Real-Time Traffic", + "network": { + "title": "Network", + "current": "Current uplink { sent } / current downlink { received }", + "total": "Total uplink { sent } / total downlink { received }" + }, + "disk": { + "title": "Disk", + "current": "Current read { read } / current write { write }", + "total": "Total read { read } / total write { write }" + } + }, + "store": { + "title": "Storage", + "columns": { + "path": "Path", + "type": "Type", + "usageRate": "Usage Rate", + "total": "Total", + "used": "Used", + "free": "Free" + } + }, + "system": { + "title": "System Information", + "columns": { + "os": "OS", + "panel": "Panel", + "uptime": "Uptime", + "operate": "Operate", + "loading": "loading..." + }, + "restart": { + "label": "Restart", + "confirm": "Are you sure you want to restart the panel?", + "success": "Restart successful", + "loading": "Restarting..." + }, + "update": { + "label": "Update", + "success": "Update successful" + } + }, + "eggs": { + "count": { + "gt10": "What are you doing, ouch!", + "gt4": "Are you awesome, Brother Kun?", + "gt0": "If you look at it one more time, it will explode." + } + }, + "about": { + "title": "About the panel", + "tnb": "The TNB development team wishes everyone a happy Dragon Boat Festival in 2024! Never downtime!", + "specialThanks": "Special thanks to the following supporters and {supporter}!", + "links": { + "group": "group", + "channel": "channel", + "supporter": "" + } + }, + "plugins": { + "title": "Quick Plugin" + } + }, + "homeUpdate": { + "title": "Update panel", + "loading": "Loading update information, please wait a moment", + "alerts": { + "success": "Panel update successful", + "info": "The panel is already the latest version" + }, + "button": { + "update": "Update" + }, + "confirm": { + "update": { + "title": "Update panel", + "content": "Are you sure you want to update the panel?", + "positiveText": "Update", + "negativeText": "Cancel", + "loading": "Panel is being updated..." + } + } + }, + "monitorIndex": { + "title": "Monitoring" + }, + "pluginIndex": { + "title": "Plugins", + "alerts": { + "info": "Click the button once, please do not click it repeatedly to avoid repeated execution!", + "warning": "It is strongly recommended to take a backup/snapshot before upgrading the plug-in to avoid being unable to roll back if problems arise!", + "setup": "Setup successful", + "install": "The task has been submitted, please check the task progress later", + "update": "The task has been submitted, please go to the task center to check the task progress", + "uninstall": "The task has been submitted, please go to the task center to check the task progress" + }, + "buttons": { + "install": "Install", + "manage": "Manage", + "update": "Upgrade", + "uninstall": "Uninstall" + }, + "confirm": { + "install": "Are you sure you want to install the plugin {plugin}?", + "update": "Upgrading the {plugin} plug-in may reset related configurations to the default state. Are you sure you want to continue?", + "uninstall": "Are you sure you want to uninstall the plugin {plugin}?" + }, + "columns": { + "name": "Name", + "description": "Description", + "installedVersion": "Installed Version", + "version": "Latest Version", + "show": "Homepage Display", + "actions": "Actions" + } + }, + "safeIndex": { + "title": "Security", + "buttons": { + "delete": "Delete", + "add": "Add", + "batchDelete": "Batch Delete" + }, + "columns": { + "port": "Port", + "protocol": "Protocol", + "actions": "Actions" + }, + "confirm": { + "delete": "Are you sure you want to delete the rule?", + "batchDelete": "High-risk operation! Are you sure you want to delete the selected rules?" + }, + "alerts": { + "undelete": "undelete", + "delete": "successfully deleted", + "add": "added successfully", + "setup": "setup successful", + "select": "Please select the rule to delete", + "ruleDelete": "Rule {rule} deleted successfully" + }, + "filter": { + "fields": { + "firewall": { + "label": "Firewall status", + "checked": "open", + "unchecked": "close" + }, + "ssh": { + "label": "SSH status", + "checked": "open", + "unchecked": "close" + }, + "ping": { + "label": "Ping status", + "checked": "open", + "unchecked": "close" + }, + "port": { + "label": "SSH port" + } + } + }, + "portControl": { + "title": "Port control", + "fields": { + "port": { + "placeholder": "Example:3306, 1000-2000" + }, + "protocol": { + "placeholder": "protocol" + } + } + } + }, + "settingIndex": { + "title": "Settings", + "info": "Update panel settings / entry, adjust browser address accordingly to access panel!", + "edit": { + "toasts": { + "success": "saved successfully" + }, + "fields": { + "name": { + "label": "Panel name", + "placeholder": "Rat Panel" + }, + "language": { + "label": "Language", + "placeholder": "Select language" + }, + "username": { + "label": "Username", + "placeholder": "admin" + }, + "password": { + "label": "Password", + "placeholder": "admin" + }, + "email": { + "label": "Email (may be useful later)", + "placeholder": "admin{'@'}example.com" + }, + "port": { + "label": "Port (After saving, restart the panel and modify the browser address bar's port to the new port to access the panel)", + "placeholder": "8888" + }, + "entrance": { + "label": "Security entrance (After saving, restart the panel and clear the browser Cookies to take effect)", + "placeholder": "admin" + }, + "https": { + "label": "Panel HTTPS" + }, + "cert": { + "label": "Certificate" + }, + "key": { + "label": "Key" + }, + "path": { + "label": "Default website directory", + "placeholder": "/www/wwwroot" + }, + "backup": { + "label": "Default backup directory", + "placeholder": "/www/backup" + } + }, + "actions": { + "submit": "save" + } + } + }, + "sshIndex": { + "title": "SSH", + "alerts": { + "save": "saved successfully" + }, + "save": { + "fields": { + "host": { + "label": "Host", + "placeholder": "Host" + }, + "port": { + "label": "Port", + "placeholder": "Port" + }, + "username": { + "label": "User name", + "placeholder": "User name" + }, + "password": { + "label": "Password", + "placeholder": "Password" + } + }, + "actions": { + "submit": "save" + } + } + }, + "taskIndex": { + "title": "Tasks", + "alerts": { + "delete": "Task has been deleted" + }, + "buttons": { + "delete": "delete", + "log": "log", + "undelete": "undelete" + }, + "confirm": { + "delete": "Are you sure you want to delete the task?" + }, + "columns": { + "name": "task name", + "status": "status", + "createdAt": "created at", + "updatedAt": "updated at", + "actions": "actions" + }, + "options": { + "status": { + "finished": "finished", + "waiting": "waiting", + "failed": "failed", + "running": "running" + } + }, + "logModal": { + "title": "Task log", + "autoRefresh": { + "on": "Auto refresh on", + "off": "Auto refresh off" + } + } + }, + "websiteIndex": { + "title": "Websites", + "columns": { + "name": "Website Name", + "status": "Status", + "path": "Path", + "remark": "Remark", + "actions": "Actions" + }, + "create": { + "trigger": "add website", + "title": "Create a new website", + "fields": { + "name": { + "label": "Website Name", + "placeholder": "Please use English for the website name. Once set, it cannot be changed." + }, + "domains": { + "label": "Domains" + }, + "port": { + "label": "Port" + }, + "phpVersion": { + "label": "PHP Version", + "placeholder": "Select PHP version" + }, + "db": { + "label": "Database", + "placeholder": "Select database" + }, + "dbName": { + "label": "Database Name", + "placeholder": "Please enter the database name" + }, + "dbUser": { + "label": "Database User", + "placeholder": "Please enter the database user" + }, + "dbPassword": { + "label": "Database Password", + "placeholder": "Please enter the database password" + }, + "path": { + "label": "Directory", + "placeholder": "Website root directory (leave blank for default site directory/site name)" + }, + "remark": { + "label": "Remark", + "placeholder": "Please enter a remark" + } + }, + "actions": { + "submit": "Submit" + } + }, + "edit": { + "trigger": "edit default page" + } + } +} diff --git a/web/src/i18n/i18n.ts b/web/src/i18n/i18n.ts new file mode 100644 index 00000000..77acbbec --- /dev/null +++ b/web/src/i18n/i18n.ts @@ -0,0 +1,27 @@ +import type { App } from 'vue' +import { createI18n } from 'vue-i18n' +import type { Composer } from 'vue-i18n' +import en from './en.json' +import zh_CN from './zh_CN.json' +import { useThemeStore } from '@/store' + +let i18n: ReturnType + +export function setupI18n(app: App) { + const themeStore = useThemeStore() + i18n = createI18n({ + legacy: false, + globalInjection: true, + locale: themeStore.language, + fallbackLocale: 'zh_CN', + messages: { + en: en, + zh_CN: zh_CN + } + }) + app.use(i18n) +} + +export const trans = (key: string, attributes = {}) => { + return (i18n.global.t as Composer['t'])(key, attributes) +} diff --git a/web/src/i18n/zh_CN.json b/web/src/i18n/zh_CN.json new file mode 100644 index 00000000..4e7d4156 --- /dev/null +++ b/web/src/i18n/zh_CN.json @@ -0,0 +1,402 @@ +{ + "name": "耗子面板", + "certIndex": { + "title": "TLS 证书" + }, + "containerIndex": { + "title": "容器管理" + }, + "cronIndex": { + "title": "计划任务" + }, + "fileIndex": { + "title": "文件管理" + }, + "homeIndex": { + "title": "仪表盘", + "website": "网站", + "database": "数据库", + "cron": "计划任务", + "sponsor": "赞助支持", + "git": "开源地址", + "resources": { + "title": "资源使用", + "cpu": { + "used": "{used} CPU / 共 {total} 线程" + }, + "memory": { + "title": "内存", + "physical": { + "used": "物理内存 使用 {used} / 总共 {total}" + }, + "swap": { + "used": "交换分区 使用 {used} / 总共 {total}" + } + } + }, + "loads": { + "title": "系统负载", + "load": "{load} 分钟负载", + "time": "近 {time} 分钟" + }, + "traffic": { + "title": "实时流量", + "network": { + "title": "网络", + "current": "实时上行 { sent }/s / 实时下行 { received }/s", + "total": "累计上行 { sent } / 累计下行 { received }" + }, + "disk": { + "title": "磁盘", + "current": "实时读取 { read }/s / 实时写入 { write }/s", + "total": "累计读取 { read } / 累计写入 { write }" + } + }, + "store": { + "title": "存储信息", + "columns": { + "path": "挂载点", + "type": "文件系统", + "usageRate": "使用率", + "total": "总共", + "used": "已用", + "free": "可用" + } + }, + "system": { + "title": "系统信息", + "columns": { + "os": "操作系统", + "panel": "面板版本", + "uptime": "运行时间", + "operate": "操作", + "loading": "加载中..." + }, + "restart": { + "label": "重启面板", + "confirm": "确定重启面板吗?", + "success": "面板重启成功", + "loading": "面板重启中..." + }, + "update": { + "label": "检查更新", + "success": "当前已是最新版本" + } + }, + "eggs": { + "count": { + "gt10": "你干嘛,哎呦!", + "gt4": "厉不厉害你坤哥", + "gt0": "在多一眼看一眼就会爆炸" + } + }, + "about": { + "title": "关于面板", + "tnb": "树新蜂开发组祝大家 2024 端午安康!永不宕机!", + "specialThanks": "特别感谢以下支持者和 {supporter}!", + "links": { + "group": "群", + "channel": "频道", + "supporter": "" + } + }, + "plugins": { + "title": "快捷插件" + } + }, + "homeUpdate": { + "title": "更新面板", + "loading": "正在加载更新信息,稍等片刻", + "alerts": { + "success": "面板更新成功", + "info": "取消更新" + }, + "button": { + "update": "立即更新" + }, + "confirm": { + "update": { + "title": "更新面板", + "content": "确定更新面板吗?", + "positiveText": "确定", + "negativeText": "取消", + "loading": "面板更新中..." + } + } + }, + "monitorIndex": { + "title": "资源监控" + }, + "pluginIndex": { + "title": "插件市场", + "alerts": { + "info": "按钮点击一次即可,请勿重复点击以免重复执行!", + "warning": "升级插件前强烈建议先备份/快照,以免出现问题时无法回滚!", + "setup": "设置成功", + "install": "任务已提交,请稍后查看任务进度", + "update": "任务已提交,请前往任务中心查看任务进度", + "uninstall": "任务已提交,请前往任务中心查看任务进度" + }, + "buttons": { + "install": "安装", + "manage": "管理", + "update": "升级", + "uninstall": "卸载" + }, + "confirm": { + "install": "确定安装插件 {plugin} 吗?", + "update": "升级 {plugin} 插件可能会重置相关配置到默认状态,确定继续吗?", + "uninstall": "确定卸载插件 {plugin} 吗?" + }, + "columns": { + "name": "插件名", + "description": "描述", + "installedVersion": "已装版本", + "version": "最新版本", + "show": "首页显示", + "actions": "操作" + } + }, + "safeIndex": { + "title": "系统安全", + "buttons": { + "delete": "删除", + "add": "添加", + "batchDelete": "批量删除" + }, + "columns": { + "port": "端口", + "protocol": "协议", + "actions": "操作" + }, + "confirm": { + "delete": "确定删除规则吗?", + "batchDelete": " 高危操作!确定删除选中的规则吗?" + }, + "alerts": { + "undelete": "取消删除", + "delete": "删除成功", + "add": "添加成功", + "setup": "设置成功", + "select": "请选择要删除的规则", + "ruleDelete": "规则 {rule} 删除成功" + }, + "filter": { + "fields": { + "firewall": { + "label": "防火墙状态", + "checked": "开启", + "unchecked": "关闭" + }, + "ssh": { + "label": "SSH状态", + "checked": "开启", + "unchecked": "关闭" + }, + "ping": { + "label": "Ping状态", + "checked": "开启", + "unchecked": "关闭" + }, + "port": { + "label": "SSH端口" + } + } + }, + "portControl": { + "title": "端口控制", + "fields": { + "port": { + "placeholder": "例如:3306, 1000-2000" + }, + "protocol": { + "placeholder": "协议" + } + } + } + }, + "settingIndex": { + "title": "面板设置", + "info": "修改面板端口 / 入口后,需要在浏览器地址栏做相应修改方可打开面板!", + "edit": { + "toasts": { + "success": "保存成功" + }, + "fields": { + "name": { + "label": "面板名称", + "placeholder": "耗子面板" + }, + "language": { + "label": "语言", + "placeholder": "选择语言" + }, + "username": { + "label": "用户名", + "placeholder": "admin" + }, + "password": { + "label": "密码", + "placeholder": "admin" + }, + "email": { + "label": "邮箱(以后可能会有用)", + "placeholder": "admin{'@'}example.com" + }, + "port": { + "label": "端口(保存后需重启面板并修改浏览器地址栏的端口为新端口以访问面板)", + "placeholder": "8888" + }, + "entrance": { + "label": "安全入口(保存后需重启面板并清除浏览器 Cookies 方可生效)", + "placeholder": "admin" + }, + "https": { + "label": "面板 HTTPS" + }, + "cert": { + "label": "证书" + }, + "key": { + "label": "密钥" + }, + "path": { + "label": "默认建站目录", + "placeholder": "/www/wwwroot" + }, + "backup": { + "label": "默认备份目录", + "placeholder": "/www/backup" + } + }, + "actions": { + "submit": "保存" + } + } + }, + "sshIndex": { + "title": "SSH 终端", + "alerts": { + "save": "保存成功" + }, + "save": { + "fields": { + "host": { + "label": "主机", + "placeholder": "主机" + }, + "port": { + "label": "端口", + "placeholder": "端口" + }, + "username": { + "label": "用户名", + "placeholder": "用户名" + }, + "password": { + "label": "密码", + "placeholder": "密码" + } + }, + "actions": { + "submit": "保存" + } + } + }, + "taskIndex": { + "title": "任务中心", + "alerts": { + "delete": "任务已删除" + }, + "buttons": { + "delete": "删除", + "log": "日志", + "undelete": "取消删除" + }, + "confirm": { + "delete": "确定删除此任务记录吗?" + }, + "columns": { + "name": "任务名", + "status": "状态", + "createdAt": "创建时间", + "updatedAt": "更新时间", + "actions": "操作" + }, + "options": { + "status": { + "finished": "已完成", + "waiting": "等待中", + "failed": "已失败", + "running": "运行中" + } + }, + "logModal": { + "title": "任务日志", + "autoRefresh": { + "on": "自动刷新开启", + "off": "自动刷新关闭" + } + } + }, + "websiteIndex": { + "title": "网站管理", + "columns": { + "name": "网站名", + "status": "运行", + "path": "目录", + "remark": "备注", + "actions": "操作" + }, + "create": { + "trigger": "新建网站", + "title": "新建网站", + "fields": { + "name": { + "label": "网站名", + "placeholder": "网站名建议使用英文,设置后不可修改" + }, + "domains": { + "label": "域名" + }, + "port": { + "label": "端口" + }, + "phpVersion": { + "label": "PHP版本", + "placeholder": "选择PHP版本" + }, + "db": { + "label": "数据库", + "placeholder": "选择数据库" + }, + "dbName": { + "label": "数据库名", + "placeholder": "数据库名" + }, + "dbUser": { + "label": "数据库用户", + "placeholder": "数据库用户" + }, + "dbPassword": { + "label": "数据库密码", + "placeholder": "数据库密码" + }, + "path": { + "label": "目录", + "placeholder": "网站根目录(不填默认为建站目录/网站名)" + }, + "remark": { + "label": "备注", + "placeholder": "备注" + } + }, + "actions": { + "submit": "创建" + } + }, + "edit": { + "trigger": "修改默认页" + } + } +} diff --git a/web/src/layout/AppMain.vue b/web/src/layout/AppMain.vue new file mode 100644 index 00000000..8de2d503 --- /dev/null +++ b/web/src/layout/AppMain.vue @@ -0,0 +1,11 @@ + + + diff --git a/web/src/layout/IndexView.vue b/web/src/layout/IndexView.vue new file mode 100644 index 00000000..a3541e4a --- /dev/null +++ b/web/src/layout/IndexView.vue @@ -0,0 +1,58 @@ + + + diff --git a/web/src/layout/header/IndexView.vue b/web/src/layout/header/IndexView.vue new file mode 100644 index 00000000..f6802455 --- /dev/null +++ b/web/src/layout/header/IndexView.vue @@ -0,0 +1,21 @@ + + + diff --git a/web/src/layout/header/components/BreadCrumb.vue b/web/src/layout/header/components/BreadCrumb.vue new file mode 100644 index 00000000..0d5c16cc --- /dev/null +++ b/web/src/layout/header/components/BreadCrumb.vue @@ -0,0 +1,63 @@ + + + diff --git a/web/src/layout/header/components/FullScreen.vue b/web/src/layout/header/components/FullScreen.vue new file mode 100644 index 00000000..8f000f66 --- /dev/null +++ b/web/src/layout/header/components/FullScreen.vue @@ -0,0 +1,12 @@ + + + diff --git a/web/src/layout/header/components/MenuCollapse.vue b/web/src/layout/header/components/MenuCollapse.vue new file mode 100644 index 00000000..a940aca1 --- /dev/null +++ b/web/src/layout/header/components/MenuCollapse.vue @@ -0,0 +1,12 @@ + + + diff --git a/web/src/layout/header/components/ReloadPage.vue b/web/src/layout/header/components/ReloadPage.vue new file mode 100644 index 00000000..2d22f952 --- /dev/null +++ b/web/src/layout/header/components/ReloadPage.vue @@ -0,0 +1,15 @@ + + + diff --git a/web/src/layout/header/components/ThemeMode.vue b/web/src/layout/header/components/ThemeMode.vue new file mode 100644 index 00000000..0d37608f --- /dev/null +++ b/web/src/layout/header/components/ThemeMode.vue @@ -0,0 +1,12 @@ + + + diff --git a/web/src/layout/header/components/UserAvatar.vue b/web/src/layout/header/components/UserAvatar.vue new file mode 100644 index 00000000..6ed5d9c3 --- /dev/null +++ b/web/src/layout/header/components/UserAvatar.vue @@ -0,0 +1,46 @@ + + + diff --git a/web/src/layout/sidebar/IndexView.vue b/web/src/layout/sidebar/IndexView.vue new file mode 100644 index 00000000..30a2a442 --- /dev/null +++ b/web/src/layout/sidebar/IndexView.vue @@ -0,0 +1,9 @@ + + + diff --git a/web/src/layout/sidebar/components/SideLogo.vue b/web/src/layout/sidebar/components/SideLogo.vue new file mode 100644 index 00000000..766d22ca --- /dev/null +++ b/web/src/layout/sidebar/components/SideLogo.vue @@ -0,0 +1,19 @@ + + + diff --git a/web/src/layout/sidebar/components/SideMenu.vue b/web/src/layout/sidebar/components/SideMenu.vue new file mode 100644 index 00000000..29f6ec43 --- /dev/null +++ b/web/src/layout/sidebar/components/SideMenu.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/web/src/layout/tab/IndexView.vue b/web/src/layout/tab/IndexView.vue new file mode 100644 index 00000000..8ba14736 --- /dev/null +++ b/web/src/layout/tab/IndexView.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/web/src/layout/tab/components/ContextMenu.vue b/web/src/layout/tab/components/ContextMenu.vue new file mode 100644 index 00000000..0c9a9b51 --- /dev/null +++ b/web/src/layout/tab/components/ContextMenu.vue @@ -0,0 +1,120 @@ + + + diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 00000000..6731c385 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,51 @@ +import '@/styles/reset.css' +import '@/styles/index.scss' +import 'uno.css' + +import { createApp } from 'vue' +import App from './App.vue' +import { setupStore, useThemeStore } from './store' +import { setupRouter } from './router' +import { setupI18n } from '@/i18n/i18n' +import { setupNaiveDiscreteApi } from './utils' +import { install as VueMonacoEditorPlugin } from '@guolao/vue-monaco-editor' +import info from '@/api/panel/info' + +async function setupApp() { + const app = createApp(App) + app.use(VueMonacoEditorPlugin, { + paths: { + vs: 'https://cdnjs.admincdn.com/monaco-editor/0.48.0/min/vs' + }, + 'vs/nls': { + availableLanguages: { '*': 'zh-cn' } + } + }) + await setupStore(app) + await setupNaiveDiscreteApi() + await setupPanel().then(() => { + setupI18n(app) + }) + await setupRouter(app) + app.mount('#app') +} + +const title = ref('') + +const setupPanel = async () => { + const themeStore = useThemeStore() + await info + .panel() + .then((response) => response.json()) + .then((data) => { + title.value = data.data.name || import.meta.env.VITE_APP_TITLE + themeStore.setLanguage(data.data.language || 'zh_CN') + }) + .catch((err) => { + console.error(err) + }) +} + +setupApp() + +export { title } diff --git a/web/src/router/guard/index.ts b/web/src/router/guard/index.ts new file mode 100644 index 00000000..8a9dd69e --- /dev/null +++ b/web/src/router/guard/index.ts @@ -0,0 +1,10 @@ +import type { Router } from 'vue-router' +import { createPageLoadingGuard } from './page-loading-guard' +import { createPageTitleGuard } from './page-title-guard' +import { createPluginInstallGuard } from './plugin-install-guard' + +export function setupRouterGuard(router: Router) { + createPageLoadingGuard(router) + createPageTitleGuard(router) + createPluginInstallGuard(router) +} diff --git a/web/src/router/guard/page-loading-guard.ts b/web/src/router/guard/page-loading-guard.ts new file mode 100644 index 00000000..577bd96c --- /dev/null +++ b/web/src/router/guard/page-loading-guard.ts @@ -0,0 +1,17 @@ +import type { Router } from 'vue-router' + +export function createPageLoadingGuard(router: Router) { + router.beforeEach(() => { + window.$loadingBar?.start() + }) + + router.afterEach(() => { + setTimeout(() => { + window.$loadingBar?.finish() + }, 200) + }) + + router.onError(() => { + window.$loadingBar?.error() + }) +} diff --git a/web/src/router/guard/page-title-guard.ts b/web/src/router/guard/page-title-guard.ts new file mode 100644 index 00000000..65e40715 --- /dev/null +++ b/web/src/router/guard/page-title-guard.ts @@ -0,0 +1,12 @@ +import type { Router } from 'vue-router' + +import { title } from '@/main' +import { trans } from '@/i18n/i18n' + +export function createPageTitleGuard(router: Router) { + router.afterEach((to) => { + const pageTitle = String(to.meta.title) + if (pageTitle) document.title = `${trans(pageTitle)} | ${title.value}` + else document.title = title.value + }) +} diff --git a/web/src/router/guard/plugin-install-guard.ts b/web/src/router/guard/plugin-install-guard.ts new file mode 100644 index 00000000..b73839e4 --- /dev/null +++ b/web/src/router/guard/plugin-install-guard.ts @@ -0,0 +1,35 @@ +import type { Router } from 'vue-router' +import plugin from '@/api/panel/plugin' + +export function createPluginInstallGuard(router: Router) { + router.beforeEach(async (to) => { + const slug = to.path.split('/').pop() + if (to.path.startsWith('/plugins/') && slug) { + await plugin.isInstalled(slug).then((res) => { + if (!res.data.installed) { + window.$message.error(`插件 ${res.data.name} 未安装`) + return router.push({ name: 'plugin-index' }) + } + }) + } + + // 网站 + if (to.path.startsWith('/website')) { + await plugin.isInstalled('openresty').then((res) => { + if (!res.data.installed) { + window.$message.error(`Web 服务器 ${res.data.name} 未安装`) + return router.push({ name: 'plugin-index' }) + } + }) + } + // 容器 + if (to.path.startsWith('/container')) { + await plugin.isInstalled('podman').then((res) => { + if (!res.data.installed) { + window.$message.error(`容器引擎 ${res.data.name} 未安装`) + return router.push({ name: 'plugin-index' }) + } + }) + } + }) +} diff --git a/web/src/router/index.ts b/web/src/router/index.ts new file mode 100644 index 00000000..fd855d8a --- /dev/null +++ b/web/src/router/index.ts @@ -0,0 +1,55 @@ +import type { App } from 'vue' +import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' +import { setupRouterGuard } from './guard' +import { basicRoutes, EMPTY_ROUTE, NOT_FOUND_ROUTE } from './routes' +import { usePermissionStore } from '@/store' +import type { RoutesType, RouteType } from '~/types/router' + +const isHash = import.meta.env.VITE_USE_HASH === 'true' +export const router = createRouter({ + history: isHash + ? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH || '/') + : createWebHistory(import.meta.env.VITE_PUBLIC_PATH || '/'), + routes: basicRoutes, + scrollBehavior: () => ({ left: 0, top: 0 }) +}) + +export async function setupRouter(app: App) { + await addDynamicRoutes() + setupRouterGuard(router) + app.use(router) +} + +export async function addDynamicRoutes() { + try { + const permissionStore = usePermissionStore() + const accessRoutes = permissionStore.generateRoutes(['admin']) + accessRoutes.forEach((route: RouteType) => { + !router.hasRoute(route.name) && router.addRoute(route) + }) + router.hasRoute(EMPTY_ROUTE.name) && router.removeRoute(EMPTY_ROUTE.name) + router.addRoute(NOT_FOUND_ROUTE) + } catch (error) { + console.error(error) + } +} + +export async function resetRouter() { + const basicRouteNames = getRouteNames(basicRoutes) + router.getRoutes().forEach((route) => { + const name = route.name as string + if (!basicRouteNames.includes(name)) router.removeRoute(name) + }) +} + +export function getRouteNames(routes: RoutesType) { + return routes.map((route) => getRouteName(route)).flat(1) +} + +function getRouteName(route: RouteType) { + const names = [route.name] + if (route.children && route.children.length) + names.push(...route.children.map((item) => getRouteName(item as RouteType)).flat(1)) + + return names +} diff --git a/web/src/router/routes/index.ts b/web/src/router/routes/index.ts new file mode 100644 index 00000000..68349862 --- /dev/null +++ b/web/src/router/routes/index.ts @@ -0,0 +1,43 @@ +import type { RouteModule, RoutesType, RouteType } from '~/types/router' + +export const basicRoutes: RoutesType = [ + { + name: '404', + path: '/404', + component: () => import('@/views/error-page/NotFound.vue'), + isHidden: true + }, + + { + name: 'Login', + path: '/login', + component: () => import('@/views/login/IndexView.vue'), + isHidden: true, + meta: { + title: '登录页' + } + } +] + +export const NOT_FOUND_ROUTE: RouteType = { + name: 'NotFound', + path: '/:pathMatch(.*)*', + redirect: '/404', + isHidden: true +} + +export const EMPTY_ROUTE: RouteType = { + name: 'Empty', + path: '/:pathMatch(.*)*', + component: () => {} +} + +const modules = import.meta.glob('@/views/**/route.ts', { + eager: true +}) as RouteModule +const asyncRoutes: RoutesType = [] +Object.keys(modules).forEach((key) => { + asyncRoutes.push(modules[key].default) +}) + +export { asyncRoutes } diff --git a/web/src/store/index.ts b/web/src/store/index.ts new file mode 100644 index 00000000..467ee86c --- /dev/null +++ b/web/src/store/index.ts @@ -0,0 +1,8 @@ +import { createPinia } from 'pinia' +import type { App } from 'vue' + +export async function setupStore(app: App) { + app.use(createPinia()) +} + +export * from './modules' diff --git a/web/src/store/modules/app/index.ts b/web/src/store/modules/app/index.ts new file mode 100644 index 00000000..bd603e9b --- /dev/null +++ b/web/src/store/modules/app/index.ts @@ -0,0 +1,22 @@ +import { defineStore } from 'pinia' + +export const useAppStore = defineStore('app', { + state() { + return { + reloadFlag: true + } + }, + actions: { + async reloadPage() { + window.$loadingBar?.start() + this.reloadFlag = false + await nextTick() + this.reloadFlag = true + + setTimeout(() => { + document.documentElement.scrollTo({ left: 0, top: 0 }) + window.$loadingBar?.finish() + }, 100) + } + } +}) diff --git a/web/src/store/modules/index.ts b/web/src/store/modules/index.ts new file mode 100644 index 00000000..a07eba4a --- /dev/null +++ b/web/src/store/modules/index.ts @@ -0,0 +1,5 @@ +export * from './app' +export * from './permission' +export * from './tab' +export * from './theme' +export * from './user' diff --git a/web/src/store/modules/permission/helpers.ts b/web/src/store/modules/permission/helpers.ts new file mode 100644 index 00000000..56d449b5 --- /dev/null +++ b/web/src/store/modules/permission/helpers.ts @@ -0,0 +1,32 @@ +import type { RoutesType, RouteType } from '~/types/router' + +function hasPermission(route: RouteType, role: string[]) { + // * 不需要权限直接返回true + if (!route.meta?.requireAuth) return true + + const routeRole = route.meta?.role ? route.meta.role : [] + + // * 登录用户没有角色或者路由没有设置角色判定为没有权限 + if (!role.length || !routeRole.length) return false + + // * 路由指定的角色包含任一登录用户角色则判定有权限 + return role.some((item) => routeRole.includes(item)) +} + +export function filterAsyncRoutes(routes: RoutesType = [], role: Array): RoutesType { + const ret: RoutesType = [] + routes.forEach((route) => { + if (hasPermission(route, role)) { + const curRoute: RouteType = { + ...route, + children: [] + } + if (route.children && route.children.length) + curRoute.children = filterAsyncRoutes(route.children, role) || [] + else Reflect.deleteProperty(curRoute, 'children') + + ret.push(curRoute) + } + }) + return ret +} diff --git a/web/src/store/modules/permission/index.ts b/web/src/store/modules/permission/index.ts new file mode 100644 index 00000000..7cc8e57c --- /dev/null +++ b/web/src/store/modules/permission/index.ts @@ -0,0 +1,30 @@ +import { defineStore } from 'pinia' +import { filterAsyncRoutes } from './helpers' +import { asyncRoutes, basicRoutes } from '@/router/routes' +import type { RoutesType } from '~/types/router' + +export const usePermissionStore = defineStore('permission', { + state() { + return { + accessRoutes: [] + } + }, + getters: { + routes(): RoutesType { + return basicRoutes.concat(this.accessRoutes) + }, + menus(): RoutesType { + return this.routes.filter((route) => route.name && !route.isHidden) + } + }, + actions: { + generateRoutes(role: Array = []): RoutesType { + const accessRoutes = filterAsyncRoutes(asyncRoutes, role) + this.accessRoutes = accessRoutes + return accessRoutes + }, + resetPermission() { + this.$reset() + } + } +}) diff --git a/web/src/store/modules/tab/helpers.ts b/web/src/store/modules/tab/helpers.ts new file mode 100644 index 00000000..e0a0c55b --- /dev/null +++ b/web/src/store/modules/tab/helpers.ts @@ -0,0 +1,6 @@ +import { getSession } from '@/utils' + +export const activeTab = getSession('activeTab') +export const tabs = getSession('tabs') + +export const WITHOUT_TAB_PATHS = ['/404', '/login'] diff --git a/web/src/store/modules/tab/index.ts b/web/src/store/modules/tab/index.ts new file mode 100644 index 00000000..fd786704 --- /dev/null +++ b/web/src/store/modules/tab/index.ts @@ -0,0 +1,65 @@ +import { defineStore } from 'pinia' +import { activeTab, tabs, WITHOUT_TAB_PATHS } from './helpers' +import { router } from '@/router' +import { setSession } from '@/utils' + +export interface TabItem { + name: string + path: string + title?: string +} + +export const useTabStore = defineStore('tab', { + state() { + return { + tabs: >tabs || [], + activeTab: activeTab || '' + } + }, + actions: { + setActiveTab(path: string) { + this.activeTab = path + setSession('activeTab', path) + }, + setTabs(tabs: Array) { + this.tabs = tabs + setSession('tabs', tabs) + }, + addTab(tab: TabItem) { + this.setActiveTab(tab.path) + if (WITHOUT_TAB_PATHS.includes(tab.path) || this.tabs.some((item) => item.path === tab.path)) + return + this.setTabs([...this.tabs, tab]) + }, + removeTab(path: string) { + if (path === this.activeTab) { + const activeIndex = this.tabs.findIndex((item) => item.path === path) + if (activeIndex > 0) router.push(this.tabs[activeIndex - 1].path) + else router.push(this.tabs[activeIndex + 1].path) + } + this.setTabs(this.tabs.filter((tab) => tab.path !== path)) + }, + removeOther(curPath: string) { + this.setTabs(this.tabs.filter((tab) => tab.path === curPath)) + if (curPath !== this.activeTab) router.push(this.tabs[this.tabs.length - 1].path) + }, + removeLeft(curPath: string) { + const curIndex = this.tabs.findIndex((item) => item.path === curPath) + const filterTabs = this.tabs.filter((item, index) => index >= curIndex) + this.setTabs(filterTabs) + if (!filterTabs.find((item) => item.path === this.activeTab)) + router.push(filterTabs[filterTabs.length - 1].path) + }, + removeRight(curPath: string) { + const curIndex = this.tabs.findIndex((item) => item.path === curPath) + const filterTabs = this.tabs.filter((item, index) => index <= curIndex) + this.setTabs(filterTabs) + if (!filterTabs.find((item) => item.path === this.activeTab)) + router.push(filterTabs[filterTabs.length - 1].path) + }, + resetTabs() { + this.setTabs([]) + this.setActiveTab('') + } + } +}) diff --git a/web/src/store/modules/theme/helpers.ts b/web/src/store/modules/theme/helpers.ts new file mode 100644 index 00000000..87e81c30 --- /dev/null +++ b/web/src/store/modules/theme/helpers.ts @@ -0,0 +1,82 @@ +import type { GlobalThemeOverrides } from 'naive-ui' +import themeSetting from '~/settings/theme.json' +import { addColorAlpha, getColorPalette } from '@/utils' + +type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error' +type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active' +type ColorKey = `${ColorType}Color${ColorScene}` +type ThemeColor = Partial> + +interface ColorAction { + scene: ColorScene + handler: (color: string) => string +} + +/** 初始化主题配置 */ +export function initThemeSettings(): Theme.Setting { + const isMobile = themeSetting.isMobile || false + const darkMode = themeSetting.darkMode || false + const sider = themeSetting.sider || { + width: 220, + collapsedWidth: 64, + collapsed: false + } + const header = themeSetting.header || { visible: true, height: 60 } + const tab = themeSetting.tab || { visible: true, height: 50 } + const primaryColor = themeSetting.primaryColor || '#66CCFF' + const otherColor = themeSetting.otherColor || { + info: '#0099ad', + success: '#52c41a', + warning: '#faad14', + error: '#f5222d' + } + const language = themeSetting.language || 'zh_CN' + return { isMobile, darkMode, sider, header, tab, primaryColor, otherColor, language } +} + +/** 获取naive的主题颜色 */ +export function getNaiveThemeOverrides(colors: Record): GlobalThemeOverrides { + const { primary, info, success, warning, error } = colors + + const themeColors = getThemeColors([ + ['primary', primary], + ['info', info], + ['success', success], + ['warning', warning], + ['error', error] + ]) + + const colorLoading = primary + + return { + common: { + ...themeColors + }, + LoadingBar: { + colorLoading + } + } +} + +/** 获取主题颜色的各种场景对应的颜色 */ +function getThemeColors(colors: [ColorType, string][]) { + const colorActions: ColorAction[] = [ + { scene: '', handler: (color) => color }, + { scene: 'Suppl', handler: (color) => color }, + { scene: 'Hover', handler: (color) => getColorPalette(color, 5) }, + { scene: 'Pressed', handler: (color) => getColorPalette(color, 7) }, + { scene: 'Active', handler: (color) => addColorAlpha(color, 0.1) } + ] + + const themeColor: ThemeColor = {} + + colors.forEach((color) => { + colorActions.forEach((action) => { + const [colorType, colorValue] = color + const colorKey: ColorKey = `${colorType}Color${action.scene}` + themeColor[colorKey] = action.handler(colorValue) + }) + }) + + return themeColor +} diff --git a/web/src/store/modules/theme/index.ts b/web/src/store/modules/theme/index.ts new file mode 100644 index 00000000..fa692e08 --- /dev/null +++ b/web/src/store/modules/theme/index.ts @@ -0,0 +1,70 @@ +import { + dateEnUS, + dateZhCN, + enUS, + type GlobalThemeOverrides, + type NDateLocale, + type NLocale, + zhCN +} from 'naive-ui' +import { darkTheme } from 'naive-ui' +import type { BuiltInGlobalTheme } from 'naive-ui/es/themes/interface' +import { defineStore } from 'pinia' +import { getNaiveThemeOverrides, initThemeSettings } from './helpers' + +type ThemeState = Theme.Setting + +const locales: Record = { + zh_CN: { locale: zhCN, dateLocale: dateZhCN }, + en: { locale: enUS, dateLocale: dateEnUS } +} + +export const useThemeStore = defineStore('theme-store', { + state: (): ThemeState => initThemeSettings(), + getters: { + naiveThemeOverrides(): GlobalThemeOverrides { + return getNaiveThemeOverrides({ + primary: this.primaryColor, + ...this.otherColor + }) + }, + naiveTheme(): BuiltInGlobalTheme | undefined { + return this.darkMode ? darkTheme : undefined + }, + naiveLocale(): NLocale { + return locales[this.language].locale + }, + naiveDateLocale(): NDateLocale { + return locales[this.language].dateLocale + } + }, + actions: { + setIsMobile(isMobile: boolean) { + this.isMobile = isMobile + }, + /** 设置暗黑模式 */ + setDarkMode(darkMode: boolean) { + this.darkMode = darkMode + }, + /** 切换/关闭 暗黑模式 */ + toggleDarkMode() { + this.darkMode = !this.darkMode + }, + /** 切换/关闭 折叠侧边栏 */ + toggleCollapsed() { + this.sider.collapsed = !this.sider.collapsed + }, + /** 设置 折叠侧边栏 */ + setCollapsed(collapsed: boolean) { + this.sider.collapsed = collapsed + }, + /** 设置主题色 */ + setPrimaryColor(color: string) { + this.primaryColor = color + }, + /** 设置语言 */ + setLanguage(language: string) { + this.language = language + } + } +}) diff --git a/web/src/store/modules/user/index.ts b/web/src/store/modules/user/index.ts new file mode 100644 index 00000000..d8325812 --- /dev/null +++ b/web/src/store/modules/user/index.ts @@ -0,0 +1,56 @@ +import { defineStore } from 'pinia' +import { toLogin } from '@/utils' +import { usePermissionStore, useTabStore } from '@/store' +import { resetRouter } from '@/router' +import user from '@/api/panel/user' + +interface UserInfo { + id?: string + username?: string + role?: Array +} + +export const useUserStore = defineStore('user', { + state() { + return { + userInfo: {} + } + }, + getters: { + userId(): string { + return this.userInfo.id || '' + }, + username(): string { + return this.userInfo.username || '' + }, + role(): Array { + return this.userInfo.role || [] + } + }, + actions: { + async getUserInfo() { + try { + const res: any = await user.info() + const { id, username, role } = res.data + this.userInfo = { id, username, role } + return Promise.resolve(res.data) + } catch (error) { + return Promise.reject(error) + } + }, + async logout() { + user.logout().then(() => { + const { resetTabs } = useTabStore() + const { resetPermission } = usePermissionStore() + resetPermission() + resetTabs() + resetRouter() + this.$reset() + toLogin() + }) + }, + setUserInfo(userInfo = {}) { + this.userInfo = { ...this.userInfo, ...userInfo } + } + } +}) diff --git a/web/src/styles/index.scss b/web/src/styles/index.scss new file mode 100644 index 00000000..04c805fa --- /dev/null +++ b/web/src/styles/index.scss @@ -0,0 +1,80 @@ +html { + font-size: 4px; // * 方便unocss计算:1单位 = 0.25rem = 1px +} + +html, +body { + width: 100%; + height: 100%; + overflow: hidden; + background-color: #f2f2f2; + font-family: 'Encode Sans Condensed', sans-serif; +} + +#app { + width: 100%; + height: 100%; +} + +/* router view transition fade-slide */ +.fade-slide-leave-active, +.fade-slide-enter-active { + transition: all 0.3s; +} + +.fade-slide-enter-from { + opacity: 0; + transform: translateX(-30px); +} + +.fade-slide-leave-to { + opacity: 0; + transform: translateX(30px); +} + +/* 自定义滚动条样式 */ +.cus-scroll { + overflow: auto; + + &::-webkit-scrollbar { + width: 8; + height: 8px; + } +} + +.cus-scroll-x { + overflow-x: auto; + + &::-webkit-scrollbar { + width: 0; + height: 8px; + } +} + +.cus-scroll-y { + overflow-y: auto; + + &::-webkit-scrollbar { + width: 8px; + height: 0; + } +} + +.cus-scroll, +.cus-scroll-x, +.cus-scroll-y { + &::-webkit-scrollbar-thumb { + background-color: transparent; + border-radius: 4px; + } + + &:hover { + &::-webkit-scrollbar-thumb { + background: #bfbfbf; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--primary-color); + } + } +} diff --git a/web/src/styles/reset.css b/web/src/styles/reset.css new file mode 100644 index 00000000..a0a76273 --- /dev/null +++ b/web/src/styles/reset.css @@ -0,0 +1,40 @@ +html { + box-sizing: border-box; +} + +*, +::before, +::after { + margin: 0; + padding: 0; + box-sizing: inherit; +} + +a { + text-decoration: none; + color: inherit; +} + +a:hover, +a:link, +a:visited, +a:active { + text-decoration: none; +} + +ol, +ul { + list-style: none; +} + +input, +textarea { + outline: none; + border: none; + resize: none; +} + +body { + font-size: 14px; + font-weight: 400; +} diff --git a/web/src/utils/auth/index.ts b/web/src/utils/auth/index.ts new file mode 100644 index 00000000..398cae73 --- /dev/null +++ b/web/src/utils/auth/index.ts @@ -0,0 +1 @@ +export * from './router' diff --git a/web/src/utils/auth/router.ts b/web/src/utils/auth/router.ts new file mode 100644 index 00000000..5cda5845 --- /dev/null +++ b/web/src/utils/auth/router.ts @@ -0,0 +1,17 @@ +import { router } from '@/router' + +export function toLogin() { + const currentRoute = unref(router.currentRoute) + const needRedirect = + !currentRoute.meta.requireAuth && !['/404', '/login'].includes(router.currentRoute.value.path) + router.replace({ + path: '/login', + query: needRedirect ? { ...currentRoute.query, redirect: currentRoute.path } : {} + }) +} + +export function toFourZeroFour() { + router.replace({ + path: '/404' + }) +} diff --git a/web/src/utils/common/color.ts b/web/src/utils/common/color.ts new file mode 100644 index 00000000..b18acc5c --- /dev/null +++ b/web/src/utils/common/color.ts @@ -0,0 +1,135 @@ +import type { HsvColor } from 'colord' +import { colord, extend } from 'colord' +import mixPlugin from 'colord/plugins/mix' + +extend([mixPlugin]) + +type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 + +const hueStep = 2 +const saturationStep = 16 +const saturationStep2 = 5 +const brightnessStep1 = 5 +const brightnessStep2 = 15 +const lightColorCount = 5 +const darkColorCount = 4 + +/** + * 根据颜色获取调色板颜色(从左至右颜色从浅到深,6为主色号) + * @param color - 颜色 + * @param index - 调色板的对应的色号(6为主色号) + * @description 算法实现从ant-design调色板算法中借鉴 https://github.com/ant-design/ant-design/blob/master/components/style/color/colorPalette.less + */ +export function getColorPalette(color: string, index: ColorIndex) { + if (index === 6) return color + + const isLight = index < 6 + const hsv = colord(color).toHsv() + const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1 + + const newHsv: HsvColor = { + h: getHue(hsv, i, isLight), + s: getSaturation(hsv, i, isLight), + v: getValue(hsv, i, isLight) + } + + return colord(newHsv).toHex() +} + +/** + * 根据颜色获取调色板颜色所有颜色 + * @param color - 颜色 + */ +export function getAllColorPalette(color: string) { + const indexs: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + return indexs.map((index) => getColorPalette(color, index)) +} + +/** + * 获取色相渐变 + * @param hsv - hsv格式颜色值 + * @param i - 与6的相对距离 + * @param isLight - 是否是亮颜色 + */ +function getHue(hsv: HsvColor, i: number, isLight: boolean) { + let hue: number + if (hsv.h >= 60 && hsv.h <= 240) { + // 冷色调 + // 减淡变亮 色相顺时针旋转 更暖 + // 加深变暗 色相逆时针旋转 更冷 + hue = isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i + } else { + // 暖色调 + // 减淡变亮 色相逆时针旋转 更暖 + // 加深变暗 色相顺时针旋转 更冷 + hue = isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i + } + if (hue < 0) hue += 360 + else if (hue >= 360) hue -= 360 + + return hue +} + +/** + * 获取饱和度渐变 + * @param hsv - hsv格式颜色值 + * @param i - 与6的相对距离 + * @param isLight - 是否是亮颜色 + */ +function getSaturation(hsv: HsvColor, i: number, isLight: boolean) { + let saturation: number + if (isLight) saturation = hsv.s - saturationStep * i + else if (i === darkColorCount) saturation = hsv.s + saturationStep + else saturation = hsv.s + saturationStep2 * i + + if (saturation > 100) saturation = 100 + + if (isLight && i === lightColorCount && saturation > 10) saturation = 10 + + if (saturation < 6) saturation = 6 + + return saturation +} + +/** + * 获取明度渐变 + * @param hsv - hsv格式颜色值 + * @param i - 与6的相对距离 + * @param isLight - 是否是亮颜色 + */ +function getValue(hsv: HsvColor, i: number, isLight: boolean) { + let value: number + if (isLight) value = hsv.v + brightnessStep1 * i + else value = hsv.v - brightnessStep2 * i + + if (value > 100) value = 100 + + return value +} + +/** + * 给颜色加透明度 + * @param color - 颜色 + * @param alpha - 透明度(0 - 1) + */ +export function addColorAlpha(color: string, alpha: number) { + return colord(color).alpha(alpha).toHex() +} + +/** + * 颜色混合 + * @param firstColor - 第一个颜色 + * @param secondColor - 第二个颜色 + * @param ratio - 第二个颜色占比 + */ +export function mixColor(firstColor: string, secondColor: string, ratio: number) { + return colord(firstColor).mix(secondColor, ratio).toHex() +} + +/** + * 是否是白颜色 + * @param color - 颜色 + */ +export function isWhiteColor(color: string) { + return colord(color).isEqual('#ffffff') +} diff --git a/web/src/utils/common/common.ts b/web/src/utils/common/common.ts new file mode 100644 index 00000000..256303c1 --- /dev/null +++ b/web/src/utils/common/common.ts @@ -0,0 +1,24 @@ +import dayjs from 'dayjs' + +type Time = undefined | string | Date + +/** 格式化时间,默认格式:YYYY-MM-DD HH:mm:ss */ +export function formatDateTime(time: Time, format = 'YYYY-MM-DD HH:mm:ss'): string { + return dayjs(time).format(format) +} + +/** 格式化日期,默认格式:YYYY-MM-DD */ +export function formatDate(date: Time = undefined, format = 'YYYY-MM-DD') { + return formatDateTime(date, format) +} + +/** 生成随机字符串 */ +export function generateRandomString(length: number) { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let result = '' + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length) + result += characters[randomIndex] + } + return result +} diff --git a/web/src/utils/common/crypto.ts b/web/src/utils/common/crypto.ts new file mode 100644 index 00000000..db2bb494 --- /dev/null +++ b/web/src/utils/common/crypto.ts @@ -0,0 +1,24 @@ +import CryptoJS from 'crypto-js' + +const CryptoSecret = '__SecretKey__' + +/** + * 加密数据 + * @param data - 数据 + */ +export function encrypto(data: any) { + const newData = JSON.stringify(data) + return CryptoJS.AES.encrypt(newData, CryptoSecret).toString() +} + +/** + * 解密数据 + * @param cipherText - 密文 + */ +export function decrypto(cipherText: string) { + const bytes = CryptoJS.AES.decrypt(cipherText, CryptoSecret) + const originalText = bytes.toString(CryptoJS.enc.Utf8) + if (originalText) return JSON.parse(originalText) + + return null +} diff --git a/web/src/utils/common/icon.ts b/web/src/utils/common/icon.ts new file mode 100644 index 00000000..25127bfd --- /dev/null +++ b/web/src/utils/common/icon.ts @@ -0,0 +1,13 @@ +import { h } from 'vue' +import { Icon } from '@iconify/vue' +import { NIcon } from 'naive-ui' + +interface Props { + size?: number + color?: string + class?: string +} + +export function renderIcon(icon: string, props: Props = { size: 12 }) { + return () => h(NIcon, props, { default: () => h(Icon, { icon }) }) +} diff --git a/web/src/utils/common/index.ts b/web/src/utils/common/index.ts new file mode 100644 index 00000000..47b433f8 --- /dev/null +++ b/web/src/utils/common/index.ts @@ -0,0 +1,6 @@ +export * from './common' +export * from './color' +export * from './crypto' +export * from './icon' +export * from './is' +export * from './naiveTools' diff --git a/web/src/utils/common/is.ts b/web/src/utils/common/is.ts new file mode 100644 index 00000000..0016d212 --- /dev/null +++ b/web/src/utils/common/is.ts @@ -0,0 +1,96 @@ +const toString = Object.prototype.toString + +export function is(val: unknown, type: string): boolean { + return toString.call(val) === `[object ${type}]` +} + +export function isDef(val: any): boolean { + return typeof val !== 'undefined' +} + +export function isUndef(val: any): boolean { + return typeof val === 'undefined' +} + +export function isNull(val: any): boolean { + return val === null +} + +export function isWhitespace(val: any): boolean { + return val === '' +} + +export function isObject(val: any): boolean { + return !isNull(val) && is(val, 'Object') +} + +export function isArray(val: any): boolean { + return val && Array.isArray(val) +} + +export function isString(val: any): boolean { + return is(val, 'String') +} + +export function isNumber(val: any): boolean { + return is(val, 'Number') +} + +export function isBoolean(val: any): boolean { + return is(val, 'Boolean') +} + +export function isDate(val: any): boolean { + return is(val, 'Date') +} + +export function isRegExp(val: any): boolean { + return is(val, 'RegExp') +} + +export function isFunction(val: any): boolean { + return typeof val === 'function' +} + +export function isPromise(val: any): boolean { + return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch) +} + +export function isElement(val: any): boolean { + return isObject(val) && !!val.tagName +} + +/** null or undefined */ +export function isNullOrUndef(val: any): boolean { + return isNull(val) || isUndef(val) +} + +/** null or undefined or 空字符 */ +export function isNullOrWhitespace(val: any): boolean { + return isNullOrUndef(val) || isWhitespace(val) +} + +/** 空数组 or 空字符 or 空map or 空set or 空对象 */ +export function isEmpty(val: any): boolean { + if (isArray(val) || isString(val)) return val.length === 0 + + if (val instanceof Map || val instanceof Set) return val.size === 0 + + if (isObject(val)) return Object.keys(val).length === 0 + + return false +} + +/** + * * 类似mysql的IFNULL函数 + * @description 当第一个参数为null/undefined/'' 则返回第二个参数作为备用值,否则返回第一个参数 + */ +export function ifNull(val: any, def: any = '') { + return isNullOrWhitespace(val) ? def : val +} + +export function isUrl(path: string): boolean { + const reg = + /(((^https?:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)$/ + return reg.test(path) +} diff --git a/web/src/utils/common/naiveTools.ts b/web/src/utils/common/naiveTools.ts new file mode 100644 index 00000000..2434829d --- /dev/null +++ b/web/src/utils/common/naiveTools.ts @@ -0,0 +1,19 @@ +import * as NaiveUI from 'naive-ui' +import { useThemeStore } from '@/store' + +export async function setupNaiveDiscreteApi() { + const themeStore = useThemeStore() + const configProviderProps = computed(() => ({ + theme: themeStore.naiveTheme, + themeOverrides: themeStore.naiveThemeOverrides + })) + const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi( + ['message', 'dialog', 'notification', 'loadingBar'], + { configProviderProps } + ) + + window.$loadingBar = loadingBar + window.$notification = notification + window.$message = message + window.$dialog = dialog +} diff --git a/web/src/utils/event/index.ts b/web/src/utils/event/index.ts new file mode 100644 index 00000000..68051e44 --- /dev/null +++ b/web/src/utils/event/index.ts @@ -0,0 +1,35 @@ +import { reactive } from 'vue' + +type EventCallback = (payload: T) => void + +interface EventBusInterface { + events: Record + emit(event: string, data?: T): void + on(event: string, callback: EventCallback): void + off(event: string, callback: EventCallback): void +} + +const EventBus: EventBusInterface = reactive({ + events: {} as Record, + emit(event: string, data?: T): void { + if (this.events[event]) { + this.events[event].forEach((callback) => callback(data)) + } + }, + on(event: string, callback: EventCallback = () => {}): void { + if (!this.events[event]) { + this.events[event] = [] + } + this.events[event].push(callback) + }, + off(event: string, callback: EventCallback): void { + if (this.events[event]) { + const index = this.events[event].indexOf(callback) + if (index > -1) { + this.events[event].splice(index, 1) + } + } + } +}) + +export default EventBus diff --git a/web/src/utils/file/index.ts b/web/src/utils/file/index.ts new file mode 100644 index 00000000..3f150124 --- /dev/null +++ b/web/src/utils/file/index.ts @@ -0,0 +1,332 @@ +const getExt = (filename: string) => { + const dot = filename.lastIndexOf('.') + if (dot === -1 || dot === 0) { + return '' + } + return filename.slice(dot + 1) +} + +const getBase = (filename: string) => { + const dot = filename.lastIndexOf('.') + if (dot === -1 || dot === 0) { + return filename + } + return filename.slice(0, dot) +} + +const getIconByExt = (ext: string) => { + switch (ext) { + case 'png': + case 'jpg': + case 'jpeg': + case 'gif': + return 'bi:file-earmark-image' + case 'mp4': + case 'avi': + case 'mkv': + case 'rmvb': + return 'bi:file-earmark-play' + case 'mp3': + case 'flac': + case 'wav': + case 'ape': + return 'bi:file-earmark-music' + case 'zip': + case 'rar': + case '7z': + case 'tar': + case 'gz': + return 'bi:file-earmark-zip' + case 'doc': + case 'docx': + case 'xls': + case 'xlsx': + return 'bi:file-earmark-word' + case 'ppt': + case 'pptx': + return 'bi:file-earmark-ppt' + case 'pdf': + return 'bi:file-earmark-pdf' + case 'txt': + case 'md': + case 'log': + case 'conf': + case 'ini': + case 'yaml': + case 'yml': + return 'bi:file-earmark-text' + case 'html': + case 'htm': + case 'xml': + case 'json': + case 'js': + case 'css': + case 'ts': + case 'vue': + case 'jsx': + case 'tsx': + case 'php': + case 'java': + case 'py': + case 'go': + case 'rb': + case 'sh': + return 'bi:file-earmark-code' + case '': + return 'bi:file-earmark-binary' + default: + return 'bi:file-earmark' + } +} + +const languageByPath = (path: string) => { + if (path.startsWith('/www/server/openresty/')) { + return 'nginx' + } + + const ext = getExt(path) + switch (ext) { + case 'abap': + return 'abap' + case 'apex': + return 'apex' + case 'azcli': + return 'azcli' + case 'bat': + return 'bat' + case 'bicep': + return 'bicep' + case 'mligo': // cameligo 扩展名 + return 'cameligo' + case 'clj': + case 'cljs': + case 'cljc': // clojure 扩展名 + return 'clojure' + case 'coffee': + return 'coffee' + case 'cpp': + case 'cc': + case 'cxx': // cpp 扩展名 + return 'cpp' + case 'cs': + return 'csharp' + case 'csp': + return 'csp' + case 'css': + return 'css' + case 'cypher': + return 'cypher' + case 'dart': + return 'dart' + case 'dockerfile': + return 'dockerfile' + case 'ecl': + return 'ecl' + case 'ex': + case 'exs': // elixir 扩展名 + return 'elixir' + case 'flow': + return 'flow9' + case 'fs': + case 'fsi': + case 'fsx': + case 'fsscript': // fsharp 扩展名 + return 'fsharp' + case 'ftl': // freemarker2 扩展名 + return 'freemarker2' + case 'go': + return 'go' + case 'graphql': + return 'graphql' + case 'handlebars': + case 'hbs': // handlebars 扩展名 + return 'handlebars' + case 'hcl': + case 'tf': // hcl 扩展名 + return 'hcl' + case 'html': + case 'htm': // html 扩展名 + return 'html' + case 'ini': + return 'ini' + case 'java': + return 'java' + case 'js': + case 'mjs': + case 'cjs': // javascript 扩展名 + return 'javascript' + case 'jl': + return 'julia' + case 'kt': + case 'kts': // kotlin 扩展名 + return 'kotlin' + case 'less': + return 'less' + case 'lex': // lexon 扩展名 + return 'lexon' + case 'lua': + return 'lua' + case 'liquid': + return 'liquid' + case 'm3': + return 'm3' + case 'md': + return 'markdown' + case 'mdx': + return 'mdx' + case 'mips': + return 'mips' + case 'dax': // msdax 扩展名 + return 'msdax' + case 'm': // objective-c 扩展名 + return 'objective-c' + case 'pas': + return 'pascal' + case 'ligo': // pascaligo 扩展名 + return 'pascaligo' + case 'pl': + return 'perl' + case 'php': + return 'php' + case 'pla': + return 'pla' + case 'dats': + case 'sats': + case 'hats': // postiats 扩展名 + return 'postiats' + case 'pq': // powerquery 扩展名 + return 'powerquery' + case 'ps1': + case 'psm1': // powershell 扩展名 + return 'powershell' + case 'proto': + return 'protobuf' + case 'pug': + case 'jade': // pug 扩展名 + return 'pug' + case 'py': + return 'python' + case 'qs': // qsharp 扩展名 + return 'qsharp' + case 'r': + return 'r' + case 'razor': + return 'razor' + case 'redis': + return 'redis' + case 'redshift': + return 'redshift' + case 'rst': + return 'restructuredtext' + case 'rb': + return 'ruby' + case 'rs': + return 'rust' + case 'sb': + return 'sb' + case 'scala': + return 'scala' + case 'scm': + case 'ss': // scheme 扩展名 + return 'scheme' + case 'scss': + return 'scss' + case 'sh': + case 'bash': // shell 扩展名 + return 'shell' + case 'sol': + return 'solidity' + case 'sophia': + return 'sophia' + case 'sparql': + return 'sparql' + case 'sql': + return 'sql' + case 'st': + return 'st' + case 'swift': + return 'swift' + case 'sv': + case 'svh': // systemverilog 扩展名 + return 'systemverilog' + case 'tcl': + return 'tcl' + case 'twig': + return 'twig' + case 'ts': + case 'tsx': // typescript 扩展名 + return 'typescript' + case 'typespec': + return 'typespec' + case 'vb': + case 'vbs': // vb 扩展名 + return 'vb' + case 'wgsl': + return 'wgsl' + case 'xml': + case 'xsd': + case 'xsl': + case 'xslt': // xml 扩展名 + return 'xml' + case 'yaml': + case 'yml': // yaml 扩展名 + return 'yaml' + default: + return '' + } +} + +const checkName = (name: string) => { + return /^[a-zA-Z0-9_.@#$%\-\s[\]()]+$/.test(name) +} + +const checkPath = (path: string) => { + return /^(?!\/)(?!.*\/$)(?!.*\/\/)(?!.*\s).*$/.test(path) +} + +const getFilename = (path: string) => { + const parts = path.split('/') + return parts.pop()! +} + +const isArchive = (name: string) => { + const ext = getExt(name) + return ['zip', 'rar', '7z', 'tar', 'gz'].includes(ext) +} + +const formatPercent = (num: any) => { + num = Number(num) + return Number(num.toFixed(2)) +} + +const formatBytes = (size: any) => { + size = Number(size) + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + let i = 0 + + while (size >= 1024 && i < units.length) { + size /= 1024 + i++ + } + + return size.toFixed(2) + ' ' + units[i] +} + +const lastDirectory = (path: string) => { + const parts = path.split('/') + return parts.pop() || '' +} + +export { + getExt, + getBase, + getIconByExt, + languageByPath, + checkName, + checkPath, + getFilename, + isArchive, + formatPercent, + formatBytes, + lastDirectory +} diff --git a/web/src/utils/http/helpers.ts b/web/src/utils/http/helpers.ts new file mode 100644 index 00000000..711a43d5 --- /dev/null +++ b/web/src/utils/http/helpers.ts @@ -0,0 +1,41 @@ +import type { ErrorResolveResponse } from '~/types/axios' +import { useUserStore } from '@/store' + +/** 自定义错误 */ +export class AxiosRejectError extends Error { + code?: number | string + data?: any + + constructor(rejectData: ErrorResolveResponse, options?: ErrorOptions) { + const { code, message, data } = rejectData + super(message, options) + this.code = code + this.data = data + } +} + +export function resolveResError(code: number | string | undefined, message = ''): string { + switch (code) { + case 400: + case 422: + message = message ?? '请求参数错误' + break + case 401: + message = message ?? '登录已过期' + useUserStore().logout() + break + case 403: + message = message ?? '没有权限' + break + case 404: + message = message ?? '资源或接口不存在' + break + case 500: + message = message ?? '服务器异常' + break + default: + message = message ?? `【${code}】: 未知异常!` + break + } + return message +} diff --git a/web/src/utils/http/index.ts b/web/src/utils/http/index.ts new file mode 100644 index 00000000..299994aa --- /dev/null +++ b/web/src/utils/http/index.ts @@ -0,0 +1,19 @@ +import axios from 'axios' +import { reqReject, reqResolve, resReject, resResolve } from './interceptors' + +export function createAxios(options = {}) { + const defaultOptions = { + timeout: 0 + } + const service = axios.create({ + ...defaultOptions, + ...options + }) + service.interceptors.request.use(reqResolve, reqReject) + service.interceptors.response.use(resResolve, resReject) + return service +} + +export const request = createAxios({ + baseURL: import.meta.env.VITE_BASE_API +}) diff --git a/web/src/utils/http/interceptors.ts b/web/src/utils/http/interceptors.ts new file mode 100644 index 00000000..877cd8c6 --- /dev/null +++ b/web/src/utils/http/interceptors.ts @@ -0,0 +1,85 @@ +import type { AxiosError, AxiosResponse } from 'axios' +import { AxiosRejectError, resolveResError } from './helpers' +import type { RequestConfig } from '~/types/axios' + +/** 请求拦截 */ +export function reqResolve(config: RequestConfig) { + return config +} + +/** 请求错误拦截 */ +export function reqReject(error: AxiosError) { + return Promise.reject(error) +} + +/** 响应拦截 */ +export function resResolve(response: AxiosResponse) { + const { data, status, config, statusText } = response + if (status !== 200) { + const code = data?.code ?? status + const message = resolveResError(code, data?.message ?? statusText) + const { noNeedTip } = config as RequestConfig + + if (!noNeedTip) { + if (code == 422) { + window.$message.error(message) + } else { + if (code != 401) { + window.$dialog.error({ + title: '请求返回异常', + content: message, + maskClosable: false + }) + } + } + } + + return Promise.reject(new AxiosRejectError({ code, message, data: data || response })) + } + + return Promise.resolve(data) +} + +/** 响应错误拦截 */ +export function resReject(error: AxiosError) { + if (!error || !error.response) { + const code = error?.code + /** 根据code处理对应的操作,并返回处理后的message */ + const message = resolveResError(code, error.message) + window.$dialog.error({ + title: '请求出现异常', + content: message, + maskClosable: false + }) + return Promise.reject(new AxiosRejectError({ code, message, data: error })) + } + const { data, status, config } = error.response + let { code, message } = data as AxiosRejectError + code = code ?? status + message = message ?? error.message + message = resolveResError(code, message) + /** 需要错误提醒 */ + const { noNeedTip } = config as RequestConfig + + if (!noNeedTip) { + if (code == 422) { + window.$message.error(message) + } else { + if (code != 401) { + window.$dialog.error({ + title: '请求返回异常', + content: message, + maskClosable: false + }) + } + } + } + + return Promise.reject( + new AxiosRejectError({ + code, + message, + data: error.response?.data || error.response + }) + ) +} diff --git a/web/src/utils/index.ts b/web/src/utils/index.ts new file mode 100644 index 00000000..f652a351 --- /dev/null +++ b/web/src/utils/index.ts @@ -0,0 +1,6 @@ +export * from './auth' +export * from './common' +export * from './event' +export * from './http' +export * from './file' +export * from './storage' diff --git a/web/src/utils/storage/index.ts b/web/src/utils/storage/index.ts new file mode 100644 index 00000000..6992c87d --- /dev/null +++ b/web/src/utils/storage/index.ts @@ -0,0 +1,2 @@ +export * from './local' +export * from './session' diff --git a/web/src/utils/storage/local.ts b/web/src/utils/storage/local.ts new file mode 100644 index 00000000..82ecae5d --- /dev/null +++ b/web/src/utils/storage/local.ts @@ -0,0 +1,55 @@ +import { decrypto, encrypto } from '@/utils' + +interface StorageData { + value: unknown + expire: number | null +} + +/** 默认缓存期限为7天 */ +const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 + +export function setLocal(key: string, value: unknown, expire: number | null = DEFAULT_CACHE_TIME) { + const storageData: StorageData = { + value, + expire: expire !== null ? new Date().getTime() + expire * 1000 : null + } + const json = encrypto(storageData) + window.localStorage.setItem(key, json) +} + +export function getLocal(key: string) { + const json = window.localStorage.getItem(key) + if (json) { + let storageData: StorageData | null = null + storageData = decrypto(json) + if (storageData) { + const { value, expire } = storageData + // 没有过期时间或者在有效期内则直接返回 + if (expire === null || expire >= Date.now()) return value as T + } + removeLocal(key) + return null + } + return null +} + +export function getLocalExpire(key: string): number | null { + const json = window.localStorage.getItem(key) + if (json) { + let storageData: StorageData | null = null + storageData = decrypto(json) + if (storageData) { + return storageData.expire + } + return null + } + return null +} + +export function removeLocal(key: string) { + window.localStorage.removeItem(key) +} + +export function clearLocal() { + window.localStorage.clear() +} diff --git a/web/src/utils/storage/session.ts b/web/src/utils/storage/session.ts new file mode 100644 index 00000000..2fc0b94a --- /dev/null +++ b/web/src/utils/storage/session.ts @@ -0,0 +1,23 @@ +import { decrypto, encrypto } from '@/utils' + +export function setSession(key: string, value: unknown) { + const json = encrypto(value) + sessionStorage.setItem(key, json) +} + +export function getSession(key: string) { + const json = sessionStorage.getItem(key) + let data: T | null = null + if (json) { + data = decrypto(json) + } + return data +} + +export function removeSession(key: string) { + window.sessionStorage.removeItem(key) +} + +export function clearSession() { + window.sessionStorage.clear() +} diff --git a/web/src/views/cert/CertView.vue b/web/src/views/cert/CertView.vue new file mode 100644 index 00000000..2a595a14 --- /dev/null +++ b/web/src/views/cert/CertView.vue @@ -0,0 +1,682 @@ + + + + + diff --git a/web/src/views/cert/DNSView.vue b/web/src/views/cert/DNSView.vue new file mode 100644 index 00000000..7265bdeb --- /dev/null +++ b/web/src/views/cert/DNSView.vue @@ -0,0 +1,380 @@ + + + + + diff --git a/web/src/views/cert/IndexView.vue b/web/src/views/cert/IndexView.vue new file mode 100644 index 00000000..b21dc016 --- /dev/null +++ b/web/src/views/cert/IndexView.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/views/cert/UserView.vue b/web/src/views/cert/UserView.vue new file mode 100644 index 00000000..94b96da0 --- /dev/null +++ b/web/src/views/cert/UserView.vue @@ -0,0 +1,353 @@ + + + + + diff --git a/web/src/views/cert/route.ts b/web/src/views/cert/route.ts new file mode 100644 index 00000000..22af1eb3 --- /dev/null +++ b/web/src/views/cert/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'cert', + path: '/cert', + component: Layout, + meta: { + order: 2 + }, + children: [ + { + name: 'cert-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'certIndex.title', + icon: 'mdi:certificate', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/cert/types.ts b/web/src/views/cert/types.ts new file mode 100644 index 00000000..ff041386 --- /dev/null +++ b/web/src/views/cert/types.ts @@ -0,0 +1,56 @@ +export interface Cert { + id: number + user_id: number + website_id: number + dns_id: number + type: string + domains: string[] + auto_renew: boolean + cert_url: string + cert: string + key: string + created_at: string + updated_at: string + website: Website + dns: DNS + user: User +} + +export interface Website { + id: number + name: string + status: boolean + path: string + php: string + ssl: boolean + remark: string + created_at: string + updated_at: string +} + +export interface DNS { + id: number + type: string + name: string + data: { + id: string + token: string + access_key: string + secret_key: string + api_key: string + } + created_at: string + updated_at: string +} + +export interface User { + id: number + email: string + ca: string + kid: string + hmac_encoded: string + private_key: string + key_type: string + created_at: string + updated_at: string +} diff --git a/web/src/views/container/ContainerCreate.vue b/web/src/views/container/ContainerCreate.vue new file mode 100644 index 00000000..426a3dac --- /dev/null +++ b/web/src/views/container/ContainerCreate.vue @@ -0,0 +1,361 @@ + + + + + diff --git a/web/src/views/container/ContainerView.vue b/web/src/views/container/ContainerView.vue new file mode 100644 index 00000000..9b490c7f --- /dev/null +++ b/web/src/views/container/ContainerView.vue @@ -0,0 +1,496 @@ + + + diff --git a/web/src/views/container/ImageView.vue b/web/src/views/container/ImageView.vue new file mode 100644 index 00000000..b4a6bc58 --- /dev/null +++ b/web/src/views/container/ImageView.vue @@ -0,0 +1,234 @@ + + + diff --git a/web/src/views/container/IndexView.vue b/web/src/views/container/IndexView.vue new file mode 100644 index 00000000..cc1f8812 --- /dev/null +++ b/web/src/views/container/IndexView.vue @@ -0,0 +1,27 @@ + + + diff --git a/web/src/views/container/NetworkView.vue b/web/src/views/container/NetworkView.vue new file mode 100644 index 00000000..68b9970f --- /dev/null +++ b/web/src/views/container/NetworkView.vue @@ -0,0 +1,328 @@ + + + diff --git a/web/src/views/container/VolumeView.vue b/web/src/views/container/VolumeView.vue new file mode 100644 index 00000000..4f1f20b8 --- /dev/null +++ b/web/src/views/container/VolumeView.vue @@ -0,0 +1,227 @@ + + + diff --git a/web/src/views/container/route.ts b/web/src/views/container/route.ts new file mode 100644 index 00000000..97b79a2d --- /dev/null +++ b/web/src/views/container/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'container', + path: '/container', + component: Layout, + meta: { + order: 5 + }, + children: [ + { + name: 'container-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'containerIndex.title', + icon: 'mdi:layers-outline', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/container/types.ts b/web/src/views/container/types.ts new file mode 100644 index 00000000..8a8fc375 --- /dev/null +++ b/web/src/views/container/types.ts @@ -0,0 +1,85 @@ +export interface ContainerList { + command: string + created: string + id: string + image: string + image_id: string + labels: { + [key: string]: string + } + name: string + ports: [ + { + IP: string + PrivatePort: number + PublicPort: number + Type: string + } + ] + state: string + status: string +} + +export interface ImageList { + id: string + created: string + containers: number + size: string + labels: { + [key: string]: string + } + repo_tags: string[] + repo_digests: string[] +} + +export interface NetworkList { + id: string + name: string + driver: string + ipv6: boolean + scope: string + internal: boolean + attachable: boolean + ingress: boolean + labels: { + [key: string]: string + } + options: { + [key: string]: string + } + ipam: { + driver: string + config: { + subnet: string + gateway: string + ip_range: string + aux_address: { + [key: string]: string + } + }[] + options: { + [key: string]: string + } + } +} + +export interface VolumeList { + id: string + created: string + driver: string + mount: string + labels: { + [key: string]: string + } + options: { + [key: string]: string + } + scope: string + status: { + [key: string]: string + } + usage: { + ref_count: number + size: string + } +} diff --git a/web/src/views/cron/IndexView.vue b/web/src/views/cron/IndexView.vue new file mode 100644 index 00000000..cb916cc8 --- /dev/null +++ b/web/src/views/cron/IndexView.vue @@ -0,0 +1,437 @@ + + + diff --git a/web/src/views/cron/route.ts b/web/src/views/cron/route.ts new file mode 100644 index 00000000..8462492f --- /dev/null +++ b/web/src/views/cron/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'cron', + path: '/cron', + component: Layout, + meta: { + order: 5 + }, + children: [ + { + name: 'cron-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'cronIndex.title', + icon: 'mdi:clock-outline', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/cron/types.ts b/web/src/views/cron/types.ts new file mode 100644 index 00000000..e0181975 --- /dev/null +++ b/web/src/views/cron/types.ts @@ -0,0 +1,11 @@ +export interface CronTask { + id: number + name: string + status: boolean + type: string + time: string + shell: string + log: string + created_at: string + updated_at: string +} diff --git a/web/src/views/error-page/NotFound.vue b/web/src/views/error-page/NotFound.vue new file mode 100644 index 00000000..736aebc0 --- /dev/null +++ b/web/src/views/error-page/NotFound.vue @@ -0,0 +1,16 @@ + + + diff --git a/web/src/views/file/ArchiveModal.vue b/web/src/views/file/ArchiveModal.vue new file mode 100644 index 00000000..3d6287da --- /dev/null +++ b/web/src/views/file/ArchiveModal.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/web/src/views/file/EditModal.vue b/web/src/views/file/EditModal.vue new file mode 100644 index 00000000..51a1fb5a --- /dev/null +++ b/web/src/views/file/EditModal.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/web/src/views/file/IndexView.vue b/web/src/views/file/IndexView.vue new file mode 100644 index 00000000..1202a60a --- /dev/null +++ b/web/src/views/file/IndexView.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/src/views/file/ListTable.vue b/web/src/views/file/ListTable.vue new file mode 100644 index 00000000..a4245193 --- /dev/null +++ b/web/src/views/file/ListTable.vue @@ -0,0 +1,412 @@ + + + + + diff --git a/web/src/views/file/PathInput.vue b/web/src/views/file/PathInput.vue new file mode 100644 index 00000000..7bd163cb --- /dev/null +++ b/web/src/views/file/PathInput.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/web/src/views/file/PermissionModal.vue b/web/src/views/file/PermissionModal.vue new file mode 100644 index 00000000..b09ff6fa --- /dev/null +++ b/web/src/views/file/PermissionModal.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/web/src/views/file/ToolBar.vue b/web/src/views/file/ToolBar.vue new file mode 100644 index 00000000..5d5b1373 --- /dev/null +++ b/web/src/views/file/ToolBar.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/web/src/views/file/UploadModal.vue b/web/src/views/file/UploadModal.vue new file mode 100644 index 00000000..0d89655f --- /dev/null +++ b/web/src/views/file/UploadModal.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/web/src/views/file/route.ts b/web/src/views/file/route.ts new file mode 100644 index 00000000..087571ce --- /dev/null +++ b/web/src/views/file/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'file', + path: '/file', + component: Layout, + meta: { + order: 6 + }, + children: [ + { + name: 'file-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'fileIndex.title', + icon: 'mdi:file-tree', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/file/types.ts b/web/src/views/file/types.ts new file mode 100644 index 00000000..bad67e3a --- /dev/null +++ b/web/src/views/file/types.ts @@ -0,0 +1,5 @@ +export interface Marked { + name: string + source: string + type: string +} diff --git a/web/src/views/home/IndexView.vue b/web/src/views/home/IndexView.vue new file mode 100644 index 00000000..729e01be --- /dev/null +++ b/web/src/views/home/IndexView.vue @@ -0,0 +1,645 @@ + + + diff --git a/web/src/views/home/UpdateView.vue b/web/src/views/home/UpdateView.vue new file mode 100644 index 00000000..06974e21 --- /dev/null +++ b/web/src/views/home/UpdateView.vue @@ -0,0 +1,86 @@ + + + diff --git a/web/src/views/home/route.ts b/web/src/views/home/route.ts new file mode 100644 index 00000000..acdcadb7 --- /dev/null +++ b/web/src/views/home/route.ts @@ -0,0 +1,38 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'home', + path: '/', + component: Layout, + redirect: '/home', + meta: { + order: 0 + }, + children: [ + { + name: 'home-index', + path: 'home', + component: () => import('./IndexView.vue'), + meta: { + title: 'homeIndex.title', + icon: 'mdi:monitor-dashboard', + role: ['admin'], + requireAuth: true + } + }, + { + name: 'home-update', + path: 'update', + component: () => import('./UpdateView.vue'), + isHidden: true, + meta: { + title: 'homeUpdate.title', + icon: 'mdi:archive-arrow-up-outline', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/home/types.ts b/web/src/views/home/types.ts new file mode 100644 index 00000000..15780b83 --- /dev/null +++ b/web/src/views/home/types.ts @@ -0,0 +1,187 @@ +interface CpuInfoStat { + cpu: number + vendorId: string + family: string + model: string + stepping: number + physicalId: string + coreId: string + cores: number + modelName: string + mhz: number + cacheSize: number + flags: string[] + microcode: string +} + +interface LoadAvgStat { + load1: number + load5: number + load15: number +} + +interface HostInfoStat { + hostname: string + uptime: number + bootTime: number + procs: number + os: string + platform: string + platformFamily: string + platformVersion: string + kernelVersion: string + kernelArch: string + virtualizationSystem: string + virtualizationRole: string + hostid: string +} + +interface VirtualMemoryStat { + total: number + available: number + used: number + usedPercent: number + free: number + active: number + inactive: number + wired: number + laundry: number + buffers: number + cached: number + writeback: number + dirty: number + writebacktmp: number + shared: number + slab: number + sreclaimable: number + sunreclaim: number + pagetables: number + swapcached: number + commitlimit: number + committedas: number + hightotal: number + highfree: number + lowtotal: number + lowfree: number + swaptotal: number + swapfree: number + mapped: number + vmalloctotal: number + vmallocused: number + vmallocchunk: number + hugepagestotal: number + hugepagesfree: number + hugepagesize: number +} + +interface SwapMemoryStat { + total: number + used: number + free: number + usedPercent: number + sin: number + sout: number + pgin: number + pgout: number + pgfault: number + pgmajfault: number +} + +interface IOCountersStat { + name: string + bytesSent: number + bytesRecv: number + packetsSent: number + packetsRecv: number + errin: number + errout: number + dropin: number + dropout: number + fifoin: number + fifoout: number +} + +interface DiskIOCountersStat { + readCount: number + mergedReadCount: number + writeCount: number + mergedWriteCount: number + readBytes: number + writeBytes: number + readTime: number + writeTime: number + iopsInProgress: number + ioTime: number + weightedIO: number + name: string + serialNumber: string + label: string +} + +interface PartitionStat { + device: string + mountpoint: string + fstype: string + opts: string +} + +interface DiskUsageStat { + path: string + fstype: string + total: number + free: number + used: number + usedPercent: number + inodesTotal: number + inodesUsed: number + inodesFree: number + inodesUsedPercent: number +} + +export interface NowMonitor { + cpus: CpuInfoStat[] + percent: number[] + load: LoadAvgStat + host: HostInfoStat + mem: VirtualMemoryStat + swap: SwapMemoryStat + net: IOCountersStat[] + disk_io: DiskIOCountersStat[] + disk: PartitionStat[] + disk_usage: DiskUsageStat[] +} + +export interface SystemInfo { + os_name: string + uptime: string + panel_version: string +} + +export interface CountInfo { + website: number + database: number + ftp: number + cron: number +} + +export interface HomePlugin { + id: number + slug: string + version: string + show: boolean + show_order: number + name: string + created_at: string + updated_at: string +} + +export interface PanelInfo { + name: string + version: string + download_name: string + download_url: string + body: string + date: string + checksums: string + checksums_url: string +} diff --git a/web/src/views/login/IndexView.vue b/web/src/views/login/IndexView.vue new file mode 100644 index 00000000..89fb44bd --- /dev/null +++ b/web/src/views/login/IndexView.vue @@ -0,0 +1,140 @@ + + + diff --git a/web/src/views/monitor/IndexView.vue b/web/src/views/monitor/IndexView.vue new file mode 100644 index 00000000..f0731e9f --- /dev/null +++ b/web/src/views/monitor/IndexView.vue @@ -0,0 +1,541 @@ + + + + + diff --git a/web/src/views/monitor/route.ts b/web/src/views/monitor/route.ts new file mode 100644 index 00000000..6c2bc18a --- /dev/null +++ b/web/src/views/monitor/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'monitor', + path: '/monitor', + component: Layout, + meta: { + order: 3 + }, + children: [ + { + name: 'monitor-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'monitorIndex.title', + icon: 'mdi:monitor', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/monitor/types.ts b/web/src/views/monitor/types.ts new file mode 100644 index 00000000..3ab88bf0 --- /dev/null +++ b/web/src/views/monitor/types.ts @@ -0,0 +1,37 @@ +interface Load { + load1: number[] + load5: number[] + load15: number[] +} + +interface Cpu { + percent: string[] +} + +interface Mem { + total: string + available: string[] + used: string[] +} + +interface Swap { + total: string + used: string[] + free: string[] +} + +interface Network { + sent: string[] + recv: string[] + tx: string[] + rx: string[] +} + +export interface MonitorData { + times: string[] + load: Load + cpu: Cpu + mem: Mem + swap: Swap + net: Network +} diff --git a/web/src/views/plugin/IndexView.vue b/web/src/views/plugin/IndexView.vue new file mode 100644 index 00000000..d5f61182 --- /dev/null +++ b/web/src/views/plugin/IndexView.vue @@ -0,0 +1,251 @@ + + + diff --git a/web/src/views/plugin/route.ts b/web/src/views/plugin/route.ts new file mode 100644 index 00000000..03d62aa1 --- /dev/null +++ b/web/src/views/plugin/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'plugin', + path: '/plugin', + component: Layout, + meta: { + order: 8 + }, + children: [ + { + name: 'plugin-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'pluginIndex.title', + icon: 'mdi:puzzle-outline', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugin/types.ts b/web/src/views/plugin/types.ts new file mode 100644 index 00000000..d17d8926 --- /dev/null +++ b/web/src/views/plugin/types.ts @@ -0,0 +1,11 @@ +export interface Plugin { + name: string + description: string + slug: string + version: string + requires: string + excludes: string + installed: boolean + installed_version: string + show: boolean +} diff --git a/web/src/views/plugins/fail2ban/IndexView.vue b/web/src/views/plugins/fail2ban/IndexView.vue new file mode 100644 index 00000000..38147b90 --- /dev/null +++ b/web/src/views/plugins/fail2ban/IndexView.vue @@ -0,0 +1,495 @@ + + + diff --git a/web/src/views/plugins/fail2ban/route.ts b/web/src/views/plugins/fail2ban/route.ts new file mode 100644 index 00000000..e8546ac4 --- /dev/null +++ b/web/src/views/plugins/fail2ban/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'fail2ban', + path: '/plugins/fail2ban', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-fail2ban-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'Fail2ban', + icon: 'mdi:wall-fire', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/fail2ban/types.ts b/web/src/views/plugins/fail2ban/types.ts new file mode 100644 index 00000000..55573429 --- /dev/null +++ b/web/src/views/plugins/fail2ban/types.ts @@ -0,0 +1,8 @@ +export interface Jail { + name: string + enabled: boolean + log_path: string + max_retry: number + find_time: number + ban_time: number +} diff --git a/web/src/views/plugins/frp/IndexView.vue b/web/src/views/plugins/frp/IndexView.vue new file mode 100644 index 00000000..245b1408 --- /dev/null +++ b/web/src/views/plugins/frp/IndexView.vue @@ -0,0 +1,229 @@ + + + diff --git a/web/src/views/plugins/frp/route.ts b/web/src/views/plugins/frp/route.ts new file mode 100644 index 00000000..f617ad2c --- /dev/null +++ b/web/src/views/plugins/frp/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'frp', + path: '/plugins/frp', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-frp-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'Frp', + icon: 'mdi:virtual-private-network', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/gitea/IndexView.vue b/web/src/views/plugins/gitea/IndexView.vue new file mode 100644 index 00000000..4bd1239e --- /dev/null +++ b/web/src/views/plugins/gitea/IndexView.vue @@ -0,0 +1,148 @@ + + + diff --git a/web/src/views/plugins/gitea/route.ts b/web/src/views/plugins/gitea/route.ts new file mode 100644 index 00000000..11f2010e --- /dev/null +++ b/web/src/views/plugins/gitea/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'gitea', + path: '/plugins/gitea', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-gitea-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'Gitea', + icon: 'mdi:git', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/mysql/IndexView.vue b/web/src/views/plugins/mysql/IndexView.vue new file mode 100644 index 00000000..9a533c55 --- /dev/null +++ b/web/src/views/plugins/mysql/IndexView.vue @@ -0,0 +1,910 @@ + + + diff --git a/web/src/views/plugins/mysql/types.ts b/web/src/views/plugins/mysql/types.ts new file mode 100644 index 00000000..721df14e --- /dev/null +++ b/web/src/views/plugins/mysql/types.ts @@ -0,0 +1,14 @@ +export interface Database { + name: string +} + +export interface User { + user: string + host: string + grants: string +} + +export interface Backup { + name: string + size: string +} diff --git a/web/src/views/plugins/mysql57/route.ts b/web/src/views/plugins/mysql57/route.ts new file mode 100644 index 00000000..4b4c6fba --- /dev/null +++ b/web/src/views/plugins/mysql57/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'mysql57', + path: '/plugins/mysql57', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-mysql57-index', + path: '', + component: () => import('../mysql/IndexView.vue'), + meta: { + title: 'MySQL 5.7', + icon: 'mdi:database', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/mysql80/route.ts b/web/src/views/plugins/mysql80/route.ts new file mode 100644 index 00000000..c1245507 --- /dev/null +++ b/web/src/views/plugins/mysql80/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'mysql80', + path: '/plugins/mysql80', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-mysql80-index', + path: '', + component: () => import('../mysql/IndexView.vue'), + meta: { + title: 'MySQL 8.0', + icon: 'mdi:database', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/mysql84/route.ts b/web/src/views/plugins/mysql84/route.ts new file mode 100644 index 00000000..fcc18689 --- /dev/null +++ b/web/src/views/plugins/mysql84/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'mysql84', + path: '/plugins/mysql84', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-mysql84-index', + path: '', + component: () => import('../mysql/IndexView.vue'), + meta: { + title: 'MySQL 8.4', + icon: 'mdi:database', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/openresty/IndexView.vue b/web/src/views/plugins/openresty/IndexView.vue new file mode 100644 index 00000000..a7faa204 --- /dev/null +++ b/web/src/views/plugins/openresty/IndexView.vue @@ -0,0 +1,278 @@ + + + diff --git a/web/src/views/plugins/openresty/route.ts b/web/src/views/plugins/openresty/route.ts new file mode 100644 index 00000000..938909db --- /dev/null +++ b/web/src/views/plugins/openresty/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'openresty', + path: '/plugins/openresty', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-openresty-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'OpenResty', + icon: 'mdi:server-network', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/openresty/types.ts b/web/src/views/plugins/openresty/types.ts new file mode 100644 index 00000000..f555524d --- /dev/null +++ b/web/src/views/plugins/openresty/types.ts @@ -0,0 +1,9 @@ +export interface Task { + id: number + name: string + status: string + shell: string + log: string + created_at: string + updated_at: string +} diff --git a/web/src/views/plugins/php/IndexView.vue b/web/src/views/plugins/php/IndexView.vue new file mode 100644 index 00000000..01d7714b --- /dev/null +++ b/web/src/views/plugins/php/IndexView.vue @@ -0,0 +1,409 @@ + + + diff --git a/web/src/views/plugins/php74/route.ts b/web/src/views/plugins/php74/route.ts new file mode 100644 index 00000000..167bb889 --- /dev/null +++ b/web/src/views/plugins/php74/route.ts @@ -0,0 +1,24 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'php74', + path: '/plugins/php74', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-php74-index', + path: '', + component: () => import('../php/IndexView.vue'), + meta: { + title: 'PHP 7.4', + icon: 'mdi:language-php', + role: ['admin'], + requireAuth: true, + php: 74 + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/php80/route.ts b/web/src/views/plugins/php80/route.ts new file mode 100644 index 00000000..afd97488 --- /dev/null +++ b/web/src/views/plugins/php80/route.ts @@ -0,0 +1,24 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'php80', + path: '/plugins/php80', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-php80-index', + path: '', + component: () => import('../php/IndexView.vue'), + meta: { + title: 'PHP 8.0', + icon: 'mdi:language-php', + role: ['admin'], + requireAuth: true, + php: 80 + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/php81/route.ts b/web/src/views/plugins/php81/route.ts new file mode 100644 index 00000000..507300a9 --- /dev/null +++ b/web/src/views/plugins/php81/route.ts @@ -0,0 +1,24 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'php81', + path: '/plugins/php81', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-php81-index', + path: '', + component: () => import('../php/IndexView.vue'), + meta: { + title: 'PHP 8.1', + icon: 'mdi:language-php', + role: ['admin'], + requireAuth: true, + php: 81 + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/php82/route.ts b/web/src/views/plugins/php82/route.ts new file mode 100644 index 00000000..17d3e23d --- /dev/null +++ b/web/src/views/plugins/php82/route.ts @@ -0,0 +1,24 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'php82', + path: '/plugins/php82', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-php82-index', + path: '', + component: () => import('../php/IndexView.vue'), + meta: { + title: 'PHP 8.2', + icon: 'mdi:language-php', + role: ['admin'], + requireAuth: true, + php: 82 + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/php83/route.ts b/web/src/views/plugins/php83/route.ts new file mode 100644 index 00000000..00baba60 --- /dev/null +++ b/web/src/views/plugins/php83/route.ts @@ -0,0 +1,24 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'php83', + path: '/plugins/php83', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-php83-index', + path: '', + component: () => import('../php/IndexView.vue'), + meta: { + title: 'PHP 8.3', + icon: 'mdi:language-php', + role: ['admin'], + requireAuth: true, + php: 83 + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/phpmyadmin/IndexView.vue b/web/src/views/plugins/phpmyadmin/IndexView.vue new file mode 100644 index 00000000..99093d1d --- /dev/null +++ b/web/src/views/plugins/phpmyadmin/IndexView.vue @@ -0,0 +1,157 @@ + + + diff --git a/web/src/views/plugins/phpmyadmin/route.ts b/web/src/views/plugins/phpmyadmin/route.ts new file mode 100644 index 00000000..162aaab9 --- /dev/null +++ b/web/src/views/plugins/phpmyadmin/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'phpmyadmin', + path: '/plugins/phpmyadmin', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-phpmyadmin-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'phpMyAdmin', + icon: 'mdi:database', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/podman/IndexView.vue b/web/src/views/plugins/podman/IndexView.vue new file mode 100644 index 00000000..3fd3d9f0 --- /dev/null +++ b/web/src/views/plugins/podman/IndexView.vue @@ -0,0 +1,194 @@ + + + diff --git a/web/src/views/plugins/podman/route.ts b/web/src/views/plugins/podman/route.ts new file mode 100644 index 00000000..9355dfd1 --- /dev/null +++ b/web/src/views/plugins/podman/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'podman', + path: '/plugins/podman', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-podman-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'Podman', + icon: 'mdi:cup-outline', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/postgresql/IndexView.vue b/web/src/views/plugins/postgresql/IndexView.vue new file mode 100644 index 00000000..6d04c01e --- /dev/null +++ b/web/src/views/plugins/postgresql/IndexView.vue @@ -0,0 +1,841 @@ + + + diff --git a/web/src/views/plugins/postgresql/types.ts b/web/src/views/plugins/postgresql/types.ts new file mode 100644 index 00000000..dd4b8e87 --- /dev/null +++ b/web/src/views/plugins/postgresql/types.ts @@ -0,0 +1,13 @@ +export interface Database { + name: string +} + +export interface Role { + role: string + attributes: string[] +} + +export interface Backup { + name: string + size: string +} diff --git a/web/src/views/plugins/postgresql15/route.ts b/web/src/views/plugins/postgresql15/route.ts new file mode 100644 index 00000000..7cff867b --- /dev/null +++ b/web/src/views/plugins/postgresql15/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'postgresql15', + path: '/plugins/postgresql15', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-postgresql15-index', + path: '', + component: () => import('../postgresql/IndexView.vue'), + meta: { + title: 'PostgreSQL 15', + icon: 'mdi:database', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/postgresql16/route.ts b/web/src/views/plugins/postgresql16/route.ts new file mode 100644 index 00000000..d8cfcb49 --- /dev/null +++ b/web/src/views/plugins/postgresql16/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'postgresql16', + path: '/plugins/postgresql16', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-postgresql16-index', + path: '', + component: () => import('../postgresql/IndexView.vue'), + meta: { + title: 'PostgreSQL 16', + icon: 'mdi:database', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/pureftpd/IndexView.vue b/web/src/views/plugins/pureftpd/IndexView.vue new file mode 100644 index 00000000..46defaa5 --- /dev/null +++ b/web/src/views/plugins/pureftpd/IndexView.vue @@ -0,0 +1,361 @@ + + + diff --git a/web/src/views/plugins/pureftpd/route.ts b/web/src/views/plugins/pureftpd/route.ts new file mode 100644 index 00000000..ad3c51c3 --- /dev/null +++ b/web/src/views/plugins/pureftpd/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'pureftpd', + path: '/plugins/pureftpd', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-pureftpd-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'Pure-FTPd', + icon: 'mdi:server-network', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/pureftpd/types.ts b/web/src/views/plugins/pureftpd/types.ts new file mode 100644 index 00000000..f7053306 --- /dev/null +++ b/web/src/views/plugins/pureftpd/types.ts @@ -0,0 +1,4 @@ +export interface User { + username: string + path: string +} diff --git a/web/src/views/plugins/redis/IndexView.vue b/web/src/views/plugins/redis/IndexView.vue new file mode 100644 index 00000000..848dbc2f --- /dev/null +++ b/web/src/views/plugins/redis/IndexView.vue @@ -0,0 +1,175 @@ + + + diff --git a/web/src/views/plugins/redis/route.ts b/web/src/views/plugins/redis/route.ts new file mode 100644 index 00000000..94df44f1 --- /dev/null +++ b/web/src/views/plugins/redis/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'redis', + path: '/plugins/redis', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-redis-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'Redis', + icon: 'mdi:database', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/rsync/IndexView.vue b/web/src/views/plugins/rsync/IndexView.vue new file mode 100644 index 00000000..0587d0f1 --- /dev/null +++ b/web/src/views/plugins/rsync/IndexView.vue @@ -0,0 +1,458 @@ + + + diff --git a/web/src/views/plugins/rsync/route.ts b/web/src/views/plugins/rsync/route.ts new file mode 100644 index 00000000..f6eb210b --- /dev/null +++ b/web/src/views/plugins/rsync/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'rsync', + path: '/plugins/rsync', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-rsync-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'Rsync', + icon: 'mdi:sync', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/rsync/types.ts b/web/src/views/plugins/rsync/types.ts new file mode 100644 index 00000000..2d309e62 --- /dev/null +++ b/web/src/views/plugins/rsync/types.ts @@ -0,0 +1,9 @@ +export interface Module { + name: string + path: string + comment: string + read_only: boolean + auth_user: string + secret: string + hosts_allow: string +} diff --git a/web/src/views/plugins/s3fs/IndexView.vue b/web/src/views/plugins/s3fs/IndexView.vue new file mode 100644 index 00000000..86f551ec --- /dev/null +++ b/web/src/views/plugins/s3fs/IndexView.vue @@ -0,0 +1,186 @@ + + + diff --git a/web/src/views/plugins/s3fs/route.ts b/web/src/views/plugins/s3fs/route.ts new file mode 100644 index 00000000..2a635c45 --- /dev/null +++ b/web/src/views/plugins/s3fs/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 's3fs', + path: '/plugins/s3fs', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-s3fs-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'S3fs', + icon: 'mdi:server-network', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/s3fs/types.ts b/web/src/views/plugins/s3fs/types.ts new file mode 100644 index 00000000..ed96d296 --- /dev/null +++ b/web/src/views/plugins/s3fs/types.ts @@ -0,0 +1,6 @@ +export interface S3fs { + id: number + path: string + bucket: string + url: string +} diff --git a/web/src/views/plugins/supervisor/IndexView.vue b/web/src/views/plugins/supervisor/IndexView.vue new file mode 100644 index 00000000..194fa414 --- /dev/null +++ b/web/src/views/plugins/supervisor/IndexView.vue @@ -0,0 +1,585 @@ + + + diff --git a/web/src/views/plugins/supervisor/route.ts b/web/src/views/plugins/supervisor/route.ts new file mode 100644 index 00000000..b0566d32 --- /dev/null +++ b/web/src/views/plugins/supervisor/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'supervisor', + path: '/plugins/supervisor', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-supervisor-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'SuperVisor', + icon: 'mdi:monitor-dashboard', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/plugins/supervisor/types.ts b/web/src/views/plugins/supervisor/types.ts new file mode 100644 index 00000000..41832467 --- /dev/null +++ b/web/src/views/plugins/supervisor/types.ts @@ -0,0 +1,6 @@ +export interface Process { + name: string + status: string + pid: string + uptime: string +} diff --git a/web/src/views/plugins/toolbox/IndexView.vue b/web/src/views/plugins/toolbox/IndexView.vue new file mode 100644 index 00000000..fb4e185c --- /dev/null +++ b/web/src/views/plugins/toolbox/IndexView.vue @@ -0,0 +1,173 @@ + + + diff --git a/web/src/views/plugins/toolbox/route.ts b/web/src/views/plugins/toolbox/route.ts new file mode 100644 index 00000000..0ef61b1e --- /dev/null +++ b/web/src/views/plugins/toolbox/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'toolbox', + path: '/plugins/toolbox', + component: Layout, + isHidden: true, + children: [ + { + name: 'plugins-toolbox-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: '系统工具箱', + icon: 'mdi:tools', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/safe/IndexView.vue b/web/src/views/safe/IndexView.vue new file mode 100644 index 00000000..12723aa9 --- /dev/null +++ b/web/src/views/safe/IndexView.vue @@ -0,0 +1,294 @@ + + + + + diff --git a/web/src/views/safe/route.ts b/web/src/views/safe/route.ts new file mode 100644 index 00000000..e002d753 --- /dev/null +++ b/web/src/views/safe/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'safe', + path: '/safe', + component: Layout, + meta: { + order: 4 + }, + children: [ + { + name: 'safe-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'safeIndex.title', + icon: 'mdi:server-security', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/safe/types.ts b/web/src/views/safe/types.ts new file mode 100644 index 00000000..956ed6e0 --- /dev/null +++ b/web/src/views/safe/types.ts @@ -0,0 +1,4 @@ +export interface FirewallRule { + port: string + protocol: string +} diff --git a/web/src/views/setting/IndexView.vue b/web/src/views/setting/IndexView.vue new file mode 100644 index 00000000..a80aa828 --- /dev/null +++ b/web/src/views/setting/IndexView.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/web/src/views/setting/SettingBase.vue b/web/src/views/setting/SettingBase.vue new file mode 100644 index 00000000..130e4b45 --- /dev/null +++ b/web/src/views/setting/SettingBase.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/web/src/views/setting/SettingHttps.vue b/web/src/views/setting/SettingHttps.vue new file mode 100644 index 00000000..b916f8b9 --- /dev/null +++ b/web/src/views/setting/SettingHttps.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/web/src/views/setting/route.ts b/web/src/views/setting/route.ts new file mode 100644 index 00000000..8cba41c4 --- /dev/null +++ b/web/src/views/setting/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'setting', + path: '/setting', + component: Layout, + meta: { + order: 10 + }, + children: [ + { + name: 'setting-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'settingIndex.title', + icon: 'mdi:settings-outline', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/ssh/IndexView.vue b/web/src/views/ssh/IndexView.vue new file mode 100644 index 00000000..025310ab --- /dev/null +++ b/web/src/views/ssh/IndexView.vue @@ -0,0 +1,164 @@ + + + diff --git a/web/src/views/ssh/route.ts b/web/src/views/ssh/route.ts new file mode 100644 index 00000000..deabfed8 --- /dev/null +++ b/web/src/views/ssh/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'ssh', + path: '/ssh', + component: Layout, + meta: { + order: 7 + }, + children: [ + { + name: 'ssh-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'sshIndex.title', + icon: 'mdi:console', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/task/IndexView.vue b/web/src/views/task/IndexView.vue new file mode 100644 index 00000000..0ba2c7f4 --- /dev/null +++ b/web/src/views/task/IndexView.vue @@ -0,0 +1,256 @@ + + + diff --git a/web/src/views/task/route.ts b/web/src/views/task/route.ts new file mode 100644 index 00000000..369d16d0 --- /dev/null +++ b/web/src/views/task/route.ts @@ -0,0 +1,25 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'task', + path: '/task', + component: Layout, + meta: { + order: 9 + }, + children: [ + { + name: 'task-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'taskIndex.title', + icon: 'mdi:archive-sync-outline', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/task/types.ts b/web/src/views/task/types.ts new file mode 100644 index 00000000..f555524d --- /dev/null +++ b/web/src/views/task/types.ts @@ -0,0 +1,9 @@ +export interface Task { + id: number + name: string + status: string + shell: string + log: string + created_at: string + updated_at: string +} diff --git a/web/src/views/website/EditView.vue b/web/src/views/website/EditView.vue new file mode 100644 index 00000000..f3388861 --- /dev/null +++ b/web/src/views/website/EditView.vue @@ -0,0 +1,428 @@ + + + diff --git a/web/src/views/website/IndexView.vue b/web/src/views/website/IndexView.vue new file mode 100644 index 00000000..8bff74ff --- /dev/null +++ b/web/src/views/website/IndexView.vue @@ -0,0 +1,783 @@ + + + diff --git a/web/src/views/website/route.ts b/web/src/views/website/route.ts new file mode 100644 index 00000000..ad1b5575 --- /dev/null +++ b/web/src/views/website/route.ts @@ -0,0 +1,37 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'website', + path: '/website', + component: Layout, + meta: { + order: 1 + }, + children: [ + { + name: 'website-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'websiteIndex.title', + icon: 'mdi:web', + role: ['admin'], + requireAuth: true + } + }, + { + name: 'website-edit', + path: 'edit/:id', + component: () => import('./EditView.vue'), + isHidden: true, + meta: { + title: '编辑网站', + icon: 'mdi:web', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType diff --git a/web/src/views/website/types.ts b/web/src/views/website/types.ts new file mode 100644 index 00000000..8beae747 --- /dev/null +++ b/web/src/views/website/types.ts @@ -0,0 +1,47 @@ +export interface Website { + id: number + name: string + status: boolean + path: string + php: number + ssl: boolean + remark: string + created_at: string + updated_at: string +} + +export interface WebsiteSetting { + name: string + ports: string[] + ssl_ports: string[] + quic_ports: string[] + domains: string[] + root: string + path: string + index: string + php: number + open_basedir: boolean + ssl: boolean + ssl_certificate: string + ssl_certificate_key: string + ssl_not_before: string + ssl_not_after: string + ssl_dns_names: string[] + ssl_issuer: string + ssl_ocsp_server: string[] + http_redirect: boolean + hsts: boolean + ocsp: boolean + waf: boolean + waf_mode: string + waf_cc_deny: string + waf_cache: string + rewrite: string + raw: string + log: string +} + +export interface Backup { + name: string + size: string +} diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 00000000..e7c3c14d --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,42 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": [ + "env.d.ts", + "src/**/*", + "types/**/*.d.ts", + "src/**/*.vue" + ], + "exclude": [ + "src/**/__tests__/*" + ], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "module": "ESNext", + "target": "ESNext", + "lib": [ + "DOM", + "ESNext" + ], + "strict": true, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "paths": { + "~/*": [ + "./*" + ], + "@/*": [ + "./src/*" + ] + }, + "types": [ + "node", + "vite/client", + "unplugin-icons/types/vue" + ] + } +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 00000000..66b5e570 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 00000000..0c69dc30 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,21 @@ +{ + "extends": [ + "@tsconfig/node20/tsconfig.json" + ], + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "paths": { + "~/*": [ + "./*" + ], + "@/*": [ + "./src/*" + ] + }, + "types": [ + "node" + ] + } +} diff --git a/web/types/axios.d.ts b/web/types/axios.d.ts new file mode 100644 index 00000000..1901d093 --- /dev/null +++ b/web/types/axios.d.ts @@ -0,0 +1,12 @@ +import type { InternalAxiosRequestConfig } from 'axios' + +interface RequestConfig extends InternalAxiosRequestConfig { + /** 接口是否需要错误提醒 */ + noNeedTip?: boolean +} + +interface ErrorResolveResponse { + code?: number | string + message: string + data?: any +} diff --git a/web/types/env.d.ts b/web/types/env.d.ts new file mode 100644 index 00000000..05b5ffe9 --- /dev/null +++ b/web/types/env.d.ts @@ -0,0 +1,20 @@ +type ProxyType = 'dev' | 'test' | 'prod' + +interface ViteEnv { + VITE_PORT: number + VITE_USE_PROXY?: boolean + VITE_USE_HASH?: boolean + VITE_APP_TITLE: string + VITE_PUBLIC_PATH: string + VITE_BASE_API: string + VITE_PROXY_TYPE?: ProxyType + VITE_USE_COMPRESS?: boolean + VITE_COMPRESS_TYPE?: 'gzip' | 'brotliCompress' | 'deflate' | 'deflateRaw' +} + +interface ProxyConfig { + /** 匹配代理的前缀,接口地址匹配到此前缀将代理的target地址 */ + prefix: string + /** 代理目标地址,后端真实接口地址 */ + target: string +} diff --git a/web/types/global.d.ts b/web/types/global.d.ts new file mode 100644 index 00000000..31ca45d7 --- /dev/null +++ b/web/types/global.d.ts @@ -0,0 +1,6 @@ +interface Window { + $loadingBar: import('naive-ui').LoadingBarProviderInst + $dialog: import('naive-ui').DialogProviderInst + $message: import('naive-ui').MessageProviderInst + $notification: import('naive-ui').NotificationProviderInst +} diff --git a/web/types/router.d.ts b/web/types/router.d.ts new file mode 100644 index 00000000..18eb7f3a --- /dev/null +++ b/web/types/router.d.ts @@ -0,0 +1,25 @@ +import { RouteRecordRaw } from 'vue-router' + +interface Meta { + title?: string + icon?: string + order?: number + role?: Array + requireAuth?: boolean +} + +interface RouteItem { + name: string + path: string + redirect?: string + isHidden?: boolean + meta?: Meta + children?: RoutesType +} + +type RouteType = RouteRecordRaw & RouteItem + +type RoutesType = Array + +/** 前端导入的路由模块 */ +type RouteModule = Record diff --git a/web/types/shims.d.ts b/web/types/shims.d.ts new file mode 100644 index 00000000..2b97bd96 --- /dev/null +++ b/web/types/shims.d.ts @@ -0,0 +1,5 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/web/types/theme.d.ts b/web/types/theme.d.ts new file mode 100644 index 00000000..5aa6c624 --- /dev/null +++ b/web/types/theme.d.ts @@ -0,0 +1,50 @@ +/** 侧边栏 */ +interface Sider { + width: number + /** 折叠时的宽度 */ + collapsedWidth: number + /** 是否折叠 */ + collapsed: boolean +} + +/** 头部样式 */ +interface Header { + /** 是否显示 */ + visible: boolean + /** 头部高度 */ + height: number +} + +/** 标多页签样式 */ +interface Tab { + /** 是否显示 */ + visible: boolean + /** 头部高度 */ + height: number +} + +interface OtherColor { + /** 信息 */ + info: string + /** 成功 */ + success: string + /** 警告 */ + warning: string + /** 错误 */ + error: string +} + +declare namespace Theme { + interface Setting { + isMobile: boolean + darkMode: boolean + sider: Sider + header: Header + tab: Tab + /** 主题颜色 */ + primaryColor: string + otherColor: OtherColor + /** 语言 */ + language: string + } +} diff --git a/web/uno.config.ts b/web/uno.config.ts new file mode 100644 index 00000000..4ca2f100 --- /dev/null +++ b/web/uno.config.ts @@ -0,0 +1,66 @@ +import type { UserConfig } from 'unocss' +import { defineConfig, presetAttributify, presetUno } from 'unocss' + +const config: UserConfig = { + content: { + pipeline: { + exclude: [ + 'node_modules', + '.git', + '.github', + '.vscode', + 'build', + 'dist', + 'public', + 'types', + './stats.html' + ] + } + }, + presets: [presetUno({ dark: 'class' }), presetAttributify()], + shortcuts: [ + ['wh-full', 'w-full h-full'], + ['f-c-c', 'flex justify-center items-center'], + ['flex-col', 'flex flex-col'], + ['absolute-lt', 'absolute left-0 top-0'], + ['absolute-lb', 'absolute left-0 bottom-0'], + ['absolute-rt', 'absolute right-0 top-0'], + ['absolute-rb', 'absolute right-0 bottom-0'], + ['absolute-center', 'absolute-lt f-c-c wh-full'], + ['text-ellipsis', 'truncate'] + ], + rules: [ + [/^bc-(.+)$/, ([, color]) => ({ 'border-color': `#${color}` })], + [ + 'card-shadow', + { 'box-shadow': '0 1px 2px -2px #00000029, 0 3px 6px #0000001f, 0 5px 12px 4px #00000017' } + ] + ], + theme: { + colors: { + primary: 'var(--primary-color)', + primary_hover: 'var(--primary-color-hover)', + primary_pressed: 'var(--primary-color-pressed)', + primary_active: 'var(--primary-color-active)', + info: 'var(--info-color)', + info_hover: 'var(--info-color-hover)', + info_pressed: 'var(--info-color-pressed)', + info_active: 'var(--info-color-active)', + success: 'var(--success-color)', + success_hover: 'var(--success-color-hover)', + success_pressed: 'var(--success-color-pressed)', + success_active: 'var(--success-color-active)', + warning: 'var(--warning-color)', + warning_hover: 'var(--warning-color-hover)', + warning_pressed: 'var(--warning-color-pressed)', + warning_active: 'var(--warning-color-active)', + error: 'var(--error-color)', + error_hover: 'var(--error-color-hover)', + error_pressed: 'var(--error-color-pressed)', + error_active: 'var(--error-color-active)', + dark: '#18181c' + } + } +} + +export default defineConfig(config) diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 00000000..853a6740 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,41 @@ +import type { ConfigEnv } from 'vite' +import { defineConfig, loadEnv } from 'vite' + +import { convertEnv, getRootPath, getSrcPath } from './build/utils' +import { createViteProxy, viteDefine } from './build/config' +import { setupVitePlugins } from './build/plugins' + +export default defineConfig((configEnv: ConfigEnv) => { + const srcPath = getSrcPath() + const rootPath = getRootPath() + const isBuild = configEnv.command === 'build' + + const viteEnv = convertEnv(loadEnv(configEnv.mode, process.cwd())) + + const { VITE_PORT, VITE_PUBLIC_PATH, VITE_USE_PROXY, VITE_PROXY_TYPE } = viteEnv + return { + base: VITE_PUBLIC_PATH, + resolve: { + alias: { + '~': rootPath, + '@': srcPath + } + }, + define: viteDefine, + plugins: setupVitePlugins(viteEnv, isBuild), + server: { + host: '0.0.0.0', + port: VITE_PORT, + open: false, + proxy: createViteProxy(VITE_USE_PROXY, VITE_PROXY_TYPE as ProxyType) + }, + build: { + reportCompressedSize: false, + sourcemap: false, + chunkSizeWarningLimit: 1024, // chunk 大小警告的限制(单位kb) + commonjsOptions: { + ignoreTryCatch: false + } + } + } +})