mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
282 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb133971db | |||
| a4e3a8d38f | |||
| ab507da99a | |||
| 8e8fad1287 | |||
| 03e9646e57 | |||
| 7613d7e73e | |||
| 6156c8737b | |||
| c25443189f | |||
| ef824b2f6d | |||
| c938da4f4f | |||
| c2bee5b6f2 | |||
| 3f339d1a53 | |||
| ea22c2f0a7 | |||
| ad4559eea3 | |||
| cdd095e7f5 | |||
| 3a2f0db45c | |||
| 4e28fc60a3 | |||
| 693dd36b30 | |||
| e46ef25e28 | |||
| ce50aa4653 | |||
| d9fee94db5 | |||
| 7f9293e562 | |||
| aca88ffeea | |||
| 634e361e86 | |||
| afbc21160f | |||
| ca49160de2 | |||
| 0e02f7ed10 | |||
| d3b5f0672a | |||
| 2ad637e1d7 | |||
| 5413926d06 | |||
| 0fc912e6a2 | |||
| b333df8d2c | |||
| e2ad3407aa | |||
| f25cd50763 | |||
| d8d1e20c5a | |||
| 9c977c154b | |||
| f347b77c72 | |||
| d356b7f5a0 | |||
| 1b184f5178 | |||
| f4a056b601 | |||
| 02f6011c14 | |||
| c0a1c177b9 | |||
| f5eea8b4a9 | |||
| f0d8acca31 | |||
| 5d314af538 | |||
| 5ad6f8fa98 | |||
| fa26e643b7 | |||
| 1528d7dfc5 | |||
| 8a205f6ec8 | |||
| 595b1c4fc7 | |||
| e0539a908a | |||
| 90634fdb93 | |||
| c01ac0cbd3 | |||
| 9b8141a5ea | |||
| 5263b1b730 | |||
| 41bf2531f1 | |||
| 71e7c5ab5d | |||
| 038ef2c44f | |||
| aba65c4cdf | |||
| 5ef34ab1ba | |||
| 83750d15c5 | |||
| 824f06970d | |||
| 7ad1b721f7 | |||
| 1f899d1e47 | |||
| a2e46faab9 | |||
| 8fa5c6d881 | |||
| 293da754ce | |||
| 0078625289 | |||
| 1bfe08e52e | |||
| 58da2ce5c3 | |||
| 8672a8456b | |||
| f00f0c7f14 | |||
| c80faa5f0f | |||
| 1baa7c03a8 | |||
| 25331a7a24 | |||
| d5a635c6dd | |||
| 8fc21039c0 | |||
| b5b5388b5c | |||
| 874abf005d | |||
| 3c7385114f | |||
| 2492534c28 | |||
| 5db94ee61c | |||
| f151e29659 | |||
| bea4654316 | |||
| 2b28b16338 | |||
| a0b5a356a8 | |||
| c9d5fb264f | |||
| 90b37f5e6a | |||
| f72e7fa31e | |||
| 1ca986e779 | |||
| a883cac315 | |||
| f904a0e53b | |||
| e6325c3348 | |||
| d02f556e1c | |||
| 2e1d403e8b | |||
| 7d2aa8401d | |||
| c2d194b489 | |||
| b5bdfdb110 | |||
| 1450c00ec6 | |||
| c2505688a6 | |||
| 349dd36cc7 | |||
| a9e16db711 | |||
| bd680a4082 | |||
| 8f226e988f | |||
| a6df473e02 | |||
| 5efb32a661 | |||
| 7fb6953773 | |||
| 7a1566b642 | |||
| a8a09de21f | |||
| cbed8e4797 | |||
| 4f1cfc4dee | |||
| 443516a2cf | |||
| 2483fabb0c | |||
| 3b02563d71 | |||
| 6f0593fbf8 | |||
| c5fb80a4fb | |||
| e1cb687bbc | |||
| 555ba71067 | |||
| 27b0cd0168 | |||
| 260690a25e | |||
| 3bdd3711b0 | |||
| 5c77144f07 | |||
| 94fe6753da | |||
| fe67fccde3 | |||
| 6b733fc2af | |||
| 44f243e94f | |||
| 756834a59a | |||
| f024381660 | |||
| 3098d9fb64 | |||
| fa71b6f4ef | |||
| 39b5f10012 | |||
| 79fa002bd6 | |||
| 00a5acbc45 | |||
| 65e2750102 | |||
| 93e013ec56 | |||
| 07e608a3c0 | |||
| a255e9e1d8 | |||
| f5ad988576 | |||
| 2e18c25054 | |||
| a98142ea93 | |||
| 4d8f6fad9f | |||
| 67a7a41469 | |||
| afae33c69a | |||
| 62c5271c37 | |||
| ac116a7680 | |||
| c1092576b2 | |||
| fea51108d2 | |||
| 69dae9be95 | |||
| e7b2323c70 | |||
| 371f80451b | |||
| 6e3b35b3db | |||
| 2798d7fb5d | |||
| f3696332af | |||
| a510b81cf5 | |||
| 4b675c587f | |||
| 3efc9e2f66 | |||
| 3715e6db5e | |||
| 29247e6944 | |||
| 6303ac943a | |||
| 8a89a40bd3 | |||
| 3589bec397 | |||
| c2a8200817 | |||
| 4a22299111 | |||
| 54354c369a | |||
| 0181dadc05 | |||
| 30b593f90a | |||
| c22d6a430d | |||
| 08b09d58e8 | |||
| 3d3188630e | |||
| f5c23b8a2a | |||
| 78718db0a5 | |||
| 072cbb0132 | |||
| c3732477be | |||
| 03d617fea1 | |||
| 39f0d2564c | |||
| c66cdfbc1f | |||
| 97332f9650 | |||
| be7aea4a68 | |||
| a71b4009fb | |||
| ef58fcd6eb | |||
| e8d19a8171 | |||
| d2ba8190ca | |||
| 31bd29ae7a | |||
| 003feef87c | |||
| 0328215564 | |||
| 3e0c1dd574 | |||
| c06f7a3629 | |||
| 14790a1551 | |||
| 21593ca29d | |||
| 1a8bd4e400 | |||
| 865f12c75b | |||
| 8c28ac19ec | |||
| ee15bfa67c | |||
| c04fdfc7aa | |||
| ded7909fdb | |||
| b5083dd8fa | |||
| c7e078b7f8 | |||
| 3a937db506 | |||
| f92c661f33 | |||
| 8c29bba051 | |||
| 40e6f8e08f | |||
| 7d0a6ced8c | |||
| 104cbc00f1 | |||
| 75c4f44262 | |||
| ef44ab8f0d | |||
| 568ce6b270 | |||
| 20df3c5f81 | |||
| 8e3999ccbe | |||
| b4efbe823f | |||
| ca218b31ec | |||
| e1823854bd | |||
| 28bd1809cc | |||
| 120baf3bd7 | |||
| cbabd963df | |||
| 6a4f91e6cc | |||
| 8792d9b5cf | |||
| e667923c51 | |||
| c2d01c4371 | |||
| 1b713c8f32 | |||
| 561db6a861 | |||
| 9f3f0c8876 | |||
| a06af8cd12 | |||
| a2c2797942 | |||
| c39f3e29b8 | |||
| 19f3d80717 | |||
| b5bdcaded6 | |||
| 419cf50a37 | |||
| b8311af661 | |||
| c8805779ed | |||
| 5a156a84cb | |||
| 10415d3ac9 | |||
| 16025c080b | |||
| f7fcd0ca8a | |||
| 220d86348c | |||
| 215a8cf4d6 | |||
| 3df31d8e7f | |||
| 430974254e | |||
| 5884d1aeed | |||
| 61769323a6 | |||
| 3e5779bd1c | |||
| d70688503b | |||
| 85cd8ef2bb | |||
| 5fd4bf2270 | |||
| 0a162cbf57 | |||
| a19bf8f4bf | |||
| 03127e7f2a | |||
| 1a58daa007 | |||
| bac3dcc86d | |||
| 4739f6ea78 | |||
| 5077e087d5 | |||
| ef9789a6b2 | |||
| 24af0f21b1 | |||
| f115e6f93c | |||
| 4b8779bfb4 | |||
| 4407d4aec2 | |||
| 5ef6074870 | |||
| 2427e90591 | |||
| 292f22a1c2 | |||
| 7439cf489e | |||
| 471570db77 | |||
| bd24e6de20 | |||
| c81c13be1a | |||
| 0bb3aea1df | |||
| 8f144e3358 | |||
| fc74228e7e | |||
| d1eec7f7d6 | |||
| 05e6a10a4e | |||
| db7e342a23 | |||
| 5495a17748 | |||
| d152f4c59f | |||
| 823635c4d8 | |||
| aa6c1fb47e | |||
| bcff04bc36 | |||
| 8aaf4e5c78 | |||
| c47ba0eac9 | |||
| 9db21de85b | |||
| a0a98d7c4e | |||
| ae6a43a780 | |||
| 6e87b0f1b3 | |||
| e76b2ee7a9 | |||
| 37a883d0bc | |||
| ac48313948 |
@@ -11,14 +11,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
@@ -44,11 +45,11 @@ jobs:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
|
||||
@@ -10,21 +10,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -12,20 +12,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: web/pnpm-lock.yaml
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
|
||||
@@ -12,12 +12,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Check Wire generated code
|
||||
run: |
|
||||
go generate ./cmd/ace ./cmd/cli
|
||||
git diff --exit-code -- cmd/ace/wire_gen.go cmd/cli/wire_gen.go
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
@@ -28,9 +32,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
@@ -42,14 +46,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -10,9 +10,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
|
||||
@@ -11,9 +11,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Run tests
|
||||
run: sudo go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
disable_search: true
|
||||
files: coverage.out
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ _cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
# 编译文件
|
||||
ace
|
||||
cli
|
||||
/ace
|
||||
/cli
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
|
||||
@@ -82,10 +82,10 @@ bash <(curl -sSLm 10 https://dl.acepanel.net/helper.sh)
|
||||
|
||||
## Star 历史
|
||||
|
||||
<a href="https://star-history.com/#acepanel/panel&Date">
|
||||
<a href="https://www.star-history.com/?repos=acepanel%2Fpanel&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=acepanel/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=acepanel/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=acepanel/panel&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=acepanel/panel&type=date&theme=dark&legend=top-left&sealed_token=A7PSvKwCQys65o30bBWFVq0j-xWQ7q0kkPMbHsRTelnAZTLIVnCIIS18tyeO0LeMqQ4fsyoN1oPeM9dPIHpa1x03AV47vz7Q880xdcO7CH-y6qzXLCMO6g" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=acepanel/panel&type=date&legend=top-left&sealed_token=A7PSvKwCQys65o30bBWFVq0j-xWQ7q0kkPMbHsRTelnAZTLIVnCIIS18tyeO0LeMqQ4fsyoN1oPeM9dPIHpa1x03AV47vz7Q880xdcO7CH-y6qzXLCMO6g" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=acepanel/panel&type=date&legend=top-left&sealed_token=A7PSvKwCQys65o30bBWFVq0j-xWQ7q0kkPMbHsRTelnAZTLIVnCIIS18tyeO0LeMqQ4fsyoN1oPeM9dPIHpa1x03AV47vz7Q880xdcO7CH-y6qzXLCMO6g" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
+16
-7
@@ -1,25 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
_ "time/tzdata"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
if os.Geteuid() != 0 {
|
||||
panic("panel must run as root")
|
||||
return errors.New("panel must run as root")
|
||||
}
|
||||
|
||||
debug.SetGCPercent(10)
|
||||
debug.SetMemoryLimit(256 << 20)
|
||||
|
||||
ace, err := initAce()
|
||||
ace, cleanup, err := initAce()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
if err = ace.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ace.Run()
|
||||
}
|
||||
|
||||
+12
-4
@@ -7,15 +7,23 @@ import (
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/http/middleware"
|
||||
"github.com/acepanel/panel/v3/internal/job"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
)
|
||||
|
||||
// initAce init application.
|
||||
func initAce() (*app.Ace, error) {
|
||||
panic(wire.Build(bootstrap.ProviderSet, middleware.ProviderSet, route.ProviderSet, service.ProviderSet, data.ProviderSet, apps.ProviderSet, job.ProviderSet, app.NewAce))
|
||||
func initAce() (*app.Ace, func(), error) {
|
||||
panic(wire.Build(
|
||||
bootstrap.ProviderSet,
|
||||
apps.ProviderSet,
|
||||
biz.ProviderSet,
|
||||
data.ProviderSet,
|
||||
service.ProviderSet,
|
||||
route.ProviderSet,
|
||||
job.ProviderSet,
|
||||
app.NewAce,
|
||||
))
|
||||
}
|
||||
|
||||
+864
-143
File diff suppressed because it is too large
Load Diff
+22
-11
@@ -1,21 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/gookit/color"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() != 0 {
|
||||
panic("panel must run as root")
|
||||
}
|
||||
|
||||
cli, err := initCli()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = cli.Run(); err != nil {
|
||||
panic(err)
|
||||
if err := run(); err != nil {
|
||||
color.Errorf("|-%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("panel must run as root")
|
||||
}
|
||||
|
||||
cli, cleanup, err := initCli()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
return cli.Run()
|
||||
}
|
||||
|
||||
+11
-5
@@ -6,14 +6,20 @@ import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/command"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
)
|
||||
|
||||
// initCli init command line.
|
||||
func initCli() (*app.Cli, error) {
|
||||
panic(wire.Build(bootstrap.ProviderSet, route.ProviderSet, service.ProviderSet, data.ProviderSet, apps.ProviderSet, app.NewCli))
|
||||
func initCli() (*app.Cli, func(), error) {
|
||||
panic(wire.Build(
|
||||
bootstrap.ProviderSet,
|
||||
biz.ProviderSet,
|
||||
data.ProviderSet,
|
||||
service.ProviderSet,
|
||||
command.ProviderSet,
|
||||
app.NewCli,
|
||||
))
|
||||
}
|
||||
|
||||
+189
-88
@@ -8,39 +8,10 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/v3/internal/apps/docker"
|
||||
"github.com/acepanel/panel/v3/internal/apps/elasticsearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/kafka"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mongodb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rocketmq"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/v3/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/v3/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/v3/internal/apps/valkey"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/command"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
)
|
||||
|
||||
@@ -50,70 +21,200 @@ import (
|
||||
|
||||
// Injectors from wire.go:
|
||||
|
||||
// initCli init command line.
|
||||
func initCli() (*app.Cli, error) {
|
||||
func initCli() (*app.Cli, func(), error) {
|
||||
config, err := bootstrap.NewConf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
db, err := bootstrap.NewDB(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
logger := bootstrap.NewLog(config)
|
||||
cacheRepo := data.NewCacheRepo(db)
|
||||
taskRunner := bootstrap.NewRunner(db, logger)
|
||||
taskRepo := data.NewTaskRepo(locale, db, logger, taskRunner)
|
||||
appRepo := data.NewAppRepo(locale, config, db, logger, cacheRepo, taskRepo)
|
||||
userRepo := data.NewUserRepo(locale, db, logger)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
settingRepo := data.NewSettingRepo(locale, db, logger, config, taskRepo)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, logger, databaseServerRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(locale, db, logger, databaseServerRepo, databaseUserRepo)
|
||||
certRepo := data.NewCertRepo(locale, db, logger, settingRepo)
|
||||
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
|
||||
websiteRepo := data.NewWebsiteRepo(locale, db, logger, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
|
||||
backupRepo := data.NewBackupRepo(locale, config, db, logger, settingRepo, websiteRepo)
|
||||
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, userPasskeyRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo, certRepo, certAccountRepo)
|
||||
cli := route.NewCli(locale, cliService)
|
||||
command := bootstrap.NewCli(locale, cli)
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
clickhouseApp := clickhouse.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
elasticsearchApp := elasticsearch.NewApp(locale)
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
giteaApp := gitea.NewApp()
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
kafkaApp := kafka.NewApp(locale)
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
mongodbApp := mongodb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
mysqlApp := mysql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(locale)
|
||||
opensearchApp := opensearch.NewApp(locale)
|
||||
perconaApp := percona.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
prometheusApp := prometheus.NewApp(locale, config, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
rocketmqApp := rocketmq.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
appCli := app.NewCli(command, gormigrate, loader)
|
||||
return appCli, nil
|
||||
logger, cleanup, err := bootstrap.NewLogger(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
slogLogger := bootstrap.NewSlog(logger)
|
||||
appRepo, err := data.NewAppRepo(config, db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cacheRepo, err := data.NewCacheRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
notifyChannelRepo, err := data.NewNotifyChannelRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
settingRepo, err := data.NewSettingRepo(config, db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
notifyUsecase, err := biz.NewNotifyUsecase(locale, slogLogger, notifyChannelRepo, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
taskRunner, err := bootstrap.NewRunner(notifyUsecase, db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
taskRepo, err := data.NewTaskRepo(db, locale, slogLogger, taskRunner)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
appUsecase, err := biz.NewAppUsecase(locale, appRepo, cacheRepo, taskRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteRepo, err := data.NewWebsiteRepo(db, locale, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupRepo, err := data.NewBackupRepo(config, db, locale, slogLogger, settingRepo, websiteRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupUsecase, err := biz.NewBackupUsecase(notifyUsecase, locale, slogLogger, backupRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cacheUsecase := biz.NewCacheUsecase(cacheRepo)
|
||||
certAccountRepo, err := data.NewCertAccountRepo(db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userRepo, err := data.NewUserRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certAccountUsecase, err := biz.NewCertAccountUsecase(locale, slogLogger, certAccountRepo, userRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certRepo, err := data.NewCertRepo(db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certUsecase, err := biz.NewCertUsecase(locale, slogLogger, certRepo, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cronRepo, err := data.NewCronRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cronUsecase := biz.NewCronUsecase(cronRepo, slogLogger)
|
||||
databaseServerRepo, err := data.NewDatabaseServerRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseServerUsecase, err := biz.NewDatabaseServerUsecase(locale, slogLogger, databaseServerRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
settingUsecase, err := biz.NewSettingUsecase(locale, slogLogger, settingRepo, taskRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userPasskeyRepo, err := data.NewUserPasskeyRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userPasskeyUsecase := biz.NewUserPasskeyUsecase(userPasskeyRepo)
|
||||
userUsecase, err := biz.NewUserUsecase(locale, slogLogger, userRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUserRepo, err := data.NewDatabaseUserRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUserUsecase, err := biz.NewDatabaseUserUsecase(slogLogger, databaseServerRepo, databaseUserRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseRepo, err := data.NewDatabaseRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUsecase, err := biz.NewDatabaseUsecase(databaseUserUsecase, locale, slogLogger, databaseRepo, databaseServerRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperRepo, err := data.NewTamperRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperUsecase, err := biz.NewTamperUsecase(notifyUsecase, settingUsecase, locale, slogLogger, tamperRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteStatUsecase := biz.NewWebsiteStatUsecase(websiteStatRepo)
|
||||
websiteUsecase, err := biz.NewWebsiteUsecase(certAccountUsecase, certUsecase, databaseUsecase, databaseUserUsecase, tamperUsecase, websiteStatUsecase, locale, slogLogger, databaseServerRepo, websiteRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cliService, err := service.NewCliService(appUsecase, backupUsecase, cacheUsecase, certAccountUsecase, certUsecase, cronUsecase, databaseServerUsecase, notifyUsecase, settingUsecase, userPasskeyUsecase, userUsecase, websiteUsecase, config, db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
v := command.Commands(locale, cliService)
|
||||
cliCommand, err := bootstrap.NewCli(locale, v)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
gormigrate, err := bootstrap.NewMigrate(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cli, err := app.NewCli(cliCommand, gormigrate)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
return cli, func() {
|
||||
cleanup()
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3,33 +3,31 @@ module github.com/acepanel/panel/v3
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/DeRuina/timberjack v1.4.2
|
||||
github.com/andybalholm/brotli v1.2.1
|
||||
github.com/bddjr/hlfhr v1.5.1
|
||||
github.com/andybalholm/brotli v1.2.2
|
||||
github.com/bddjr/hlfhr v1.6.1
|
||||
github.com/beevik/ntp v1.5.0
|
||||
github.com/cilium/ebpf v0.21.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/cilium/ebpf v0.22.0
|
||||
github.com/coder/websocket v1.8.15
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/dchest/captcha v1.1.0
|
||||
github.com/expr-lang/expr v1.17.8
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/httplog/v3 v3.3.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-chi/chi/v5 v5.3.1
|
||||
github.com/go-chi/httplog/v3 v3.4.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/go-webauthn/webauthn v0.17.2
|
||||
github.com/go-webauthn/webauthn v0.17.4
|
||||
github.com/gomodule/redigo v1.9.3
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/gookit/validate v1.5.7
|
||||
github.com/hashicorp/go-version v1.9.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/libdns/alidns v1.0.7
|
||||
github.com/libdns/cloudflare v0.2.2
|
||||
github.com/libdns/cloudns v1.1.0
|
||||
github.com/libdns/cloudns v1.2.0
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833
|
||||
github.com/libdns/huaweicloud v1.0.1
|
||||
github.com/libdns/libdns v1.1.1
|
||||
@@ -37,69 +35,76 @@ require (
|
||||
github.com/libdns/porkbun v1.1.0
|
||||
github.com/libdns/tencentcloud v1.4.3
|
||||
github.com/libdns/westcn v1.0.2
|
||||
github.com/libtnb/cache v1.2.0
|
||||
github.com/libtnb/chix v1.3.2
|
||||
github.com/libtnb/cron v0.2.0
|
||||
github.com/libtnb/gormstore v1.2.0
|
||||
github.com/libtnb/sessions v1.4.0
|
||||
github.com/libtnb/sqlite v1.1.1
|
||||
github.com/libtnb/utils v1.2.1
|
||||
github.com/medama-io/go-useragent v1.2.3
|
||||
github.com/libtnb/chix/v2 v2.1.1
|
||||
github.com/libtnb/cron v0.5.2
|
||||
github.com/libtnb/gormstore v1.3.0
|
||||
github.com/libtnb/logrotate v0.1.2
|
||||
github.com/libtnb/sessions v1.5.0
|
||||
github.com/libtnb/sqlite v1.2.1
|
||||
github.com/libtnb/utils v1.2.2
|
||||
github.com/libtnb/validator v0.4.1
|
||||
github.com/libtnb/validator/contrib/openapi v0.2.0
|
||||
github.com/medama-io/go-useragent v1.2.4
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/moby/moby/api v1.54.2
|
||||
github.com/moby/moby/client v0.4.1
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/pkg/sftp v1.13.11
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rhnvrm/simples3 v0.11.1
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/sethvargo/go-limiter v1.1.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.4
|
||||
github.com/sethvargo/go-limiter v1.2.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.6
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/studio-b12/gowebdav v0.12.0
|
||||
github.com/studio-b12/gowebdav v0.13.0
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3
|
||||
github.com/urfave/cli/v3 v3.8.0
|
||||
github.com/urfave/cli/v3 v3.10.1
|
||||
github.com/valyala/fastjson v1.6.10
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
gorm.io/gorm v1.31.1
|
||||
modernc.org/sqlite v1.50.0
|
||||
resty.dev/v3 v3.0.0-beta.6
|
||||
github.com/wneessen/go-mail v0.8.1
|
||||
github.com/xuri/excelize/v2 v2.11.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
gorm.io/gorm v1.31.2
|
||||
modernc.org/sqlite v1.55.0
|
||||
resty.dev/v3 v3.0.0-rc.3
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/bddjr/shuttingdown v0.1.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/boyter/go-string v1.0.5 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // 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.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.3 // indirect
|
||||
github.com/gofiber/schema v1.7.1 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.6 // indirect
|
||||
github.com/gofiber/schema v1.8.2 // indirect
|
||||
github.com/gofiber/utils/v2 v2.2.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/subcommands v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/filter v1.2.3 // indirect
|
||||
github.com/gookit/goutil v0.7.4 // indirect
|
||||
github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef // 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/klauspost/compress v1.18.5 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/libtnb/securecookie v1.2.0 // indirect
|
||||
github.com/libtnb/securecookie v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
@@ -107,10 +112,13 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.7 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||
github.com/timtadh/data-structures v0.6.2 // indirect
|
||||
github.com/timtadh/lexmachine v0.2.3 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
@@ -118,21 +126,26 @@ require (
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
modernc.org/libc v1.74.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/mholt/acmez/v3 => github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292
|
||||
github.com/moby/moby/client => github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b
|
||||
github.com/rhnvrm/simples3 => github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff
|
||||
github.com/stretchr/testify => github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7
|
||||
)
|
||||
|
||||
tool github.com/google/wire
|
||||
tool github.com/google/wire/cmd/wire
|
||||
|
||||
@@ -17,8 +17,6 @@ filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
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/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
|
||||
github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3 h1:McILJSbJ5nOcT0MI0aBYhEuufCF329YbqKwFIN0RjCI=
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -26,13 +24,15 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
|
||||
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/bddjr/hlfhr v1.5.1 h1:FxphatB9VSv8AV/50jwXX3QR12IJFD7fv2P99NymXqs=
|
||||
github.com/bddjr/hlfhr v1.5.1/go.mod h1:YmFjslYd0ulNO0CtykhkhRUXJpfdq9sPVHQ4G67D610=
|
||||
github.com/bddjr/hlfhr v1.6.1 h1:bzl0zsL2pl7Gvlbx6l8jZjjv9GbBTeOaX4/+RaXyNXc=
|
||||
github.com/bddjr/hlfhr v1.6.1/go.mod h1:cHRve//dit7d8ZCPUUFZAYW5WCm3LdMoiqX9i9GJdms=
|
||||
github.com/bddjr/shuttingdown v0.1.0 h1:1thUjnzTXNbzexEFkh638ebjln8F4KlqXfLIlBXfdbs=
|
||||
github.com/bddjr/shuttingdown v0.1.0/go.mod h1:+vG4Zp8uhFLT40DBABiA/XxJDHbzybe8RR72i29DFI4=
|
||||
github.com/beevik/ntp v1.5.0 h1:y+uj/JjNwlY2JahivxYvtmv4ehfi3h74fAuABB9ZSM4=
|
||||
github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -45,11 +45,11 @@ github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
|
||||
github.com/boyter/go-string v1.0.5 h1:/xcOlWdgelLYLVkUU0xBLfioGjZ9KIMUMI/RXG138YY=
|
||||
github.com/boyter/go-string v1.0.5/go.mod h1:Mww9cDld2S2cdJ0tQffBhsZFMQRA2OJdcjWYZXvZ4Ss=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k=
|
||||
github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ=
|
||||
github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY=
|
||||
github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
@@ -67,14 +67,12 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/captcha v1.1.0 h1:2kt47EoYUUkaISobUdTbqwx55xvKOJxyScVfw25xzhQ=
|
||||
github.com/dchest/captcha v1.1.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo=
|
||||
github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff h1:R8fAbRKWR+NVWIkjdK45CXS2cZzXQn8QuoM0Dij2Jso=
|
||||
github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff/go.mod h1:c2xW30bukipkBlWNnXG1wDjq3gykQ6ww2AB/9NHMLMY=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
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/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -84,30 +82,35 @@ github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLA
|
||||
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
|
||||
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
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=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/httplog/v3 v3.3.0 h1:Gr6Y7nSzbpyCyRwKPOVKjDH3BH6TH5uvRNDsTZWDpvU=
|
||||
github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFyokPLe6y2w=
|
||||
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
|
||||
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/httplog/v3 v3.4.0 h1:gO4fvt8HEtFwHq926HoKe1aV2DymfPJuZy4+U4zwT3I=
|
||||
github.com/go-chi/httplog/v3 v3.4.0/go.mod h1:tDhJo9G+F4mioDgX4pKbyA0uVZwCtHejoSsDkvJkFkU=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5 h1:1OyorA5LtdQw12cyJDEHuTrEV3GiXiIhS4/QTTa/SM8=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5/go.mod h1:mj9ekk/7CPF3VjopaFvWKN2v7fN3D9d3eEOAXRhi/+M=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
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/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
@@ -118,14 +121,14 @@ github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.17.2 h1:e9YtSZTVnxnMWFezXi6JvnqOSxmH4Er8QDHK2a/mM40=
|
||||
github.com/go-webauthn/webauthn v0.17.2/go.mod h1:mQC6L0lZ5Kiu35G70zeB2WnrW4+vbHjR8Koq4HdVaMg=
|
||||
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
|
||||
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
|
||||
github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
|
||||
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
|
||||
github.com/gofiber/utils/v2 v2.0.0 h1:SCC3rpsEDWupFSHtc0RKxg/BKgV0s1qKfZg9Jv6D0sM=
|
||||
github.com/gofiber/utils/v2 v2.0.0/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE=
|
||||
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
|
||||
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
|
||||
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
|
||||
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
|
||||
github.com/gofiber/schema v1.8.2 h1:wq+LO2xEGlsqma/8Akp9PUebQ6vcsYmF0xYQ4F2ijvU=
|
||||
github.com/gofiber/schema v1.8.2/go.mod h1:iyAMJztdyky7Pk2U7bwUP3EHjzMqUNjZ/hglE0nYO/g=
|
||||
github.com/gofiber/utils/v2 v2.2.0 h1:YSSmCzQponq/f9uSOg2HtXC5qK1Dmor0o6DqaQVz8GE=
|
||||
github.com/gofiber/utils/v2 v2.2.0/go.mod h1:Ieopk6sQh7rbhQ12aBNCJtJuG0gxAg0nz63sFCrrOmE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
@@ -150,14 +153,14 @@ github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
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-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
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.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
@@ -168,12 +171,6 @@ github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gookit/filter v1.2.3 h1:Zo7cBOtsVzAoa/jtf+Ury6zlsbJXqInFdUpbbnB2vMM=
|
||||
github.com/gookit/filter v1.2.3/go.mod h1:nFLJcOV8dRgS1iiX23gUQgmHUhpuS40qCvAGgIvA1pM=
|
||||
github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM=
|
||||
github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/validate v1.5.7 h1:W0nqxAbgCJ4ND/oR+Tx8yyYn6l+PkFVvGHlG7LxpYsc=
|
||||
github.com/gookit/validate v1.5.7/go.mod h1:VeThM1saXWVJIG/Yn627PJL9yvap0s4bsjPUIV8I0UI=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
@@ -226,8 +223,6 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
@@ -251,8 +246,8 @@ github.com/libdns/alidns v1.0.7 h1:0BhTUFlWIUyb5U7wujGqsyoeZkleUR/hMCtdZDS8Oc8=
|
||||
github.com/libdns/alidns v1.0.7/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
|
||||
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
|
||||
github.com/libdns/cloudns v1.1.0 h1:W+1MadtxKySn3b5RITFTsXgTIvr5VoO5x97cewjlDcs=
|
||||
github.com/libdns/cloudns v1.1.0/go.mod h1:/22V6tYYDALDpM4pw/RGGJ+X2F1Luibty9kKpKvkqBM=
|
||||
github.com/libdns/cloudns v1.2.0 h1:V299bWj6HHTsXSzoEkRsKNBb9V/HGj3zp4OYzJ31JX8=
|
||||
github.com/libdns/cloudns v1.2.0/go.mod h1:zKx/cXy9W3f4bGjbEtOx+B435flBrz1uibNr3h27+aU=
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833 h1:/gsawtsq03cI7qgK65v16tYHIh40omL9YEzHWqnc63I=
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833/go.mod h1:jZJEV7pCTOJFlaUhHty+YwR05dzoSmInXK/vT3wOeVg=
|
||||
github.com/libdns/huaweicloud v1.0.1 h1:TdfpPY//3/QPhEGCyvR8u2DTlGxzpyaIsqofH5kYFyA=
|
||||
@@ -269,26 +264,28 @@ github.com/libdns/westcn v1.0.2 h1:PA2M3tME5/0T3klPMzSHvGk1EWnGNNkJiKxaJjFalnM=
|
||||
github.com/libdns/westcn v1.0.2/go.mod h1:iKpk8jjOU+793Yp8nHoihPTCR6N1KWtm4/r8BB9mVnk=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292 h1:FF1ZlwFFjpwT6CtBodr/NV4OMJPMC7plTmfzqhXrlSk=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||
github.com/libtnb/cache v1.2.0 h1:6MHHRTzP2GgNs1gD9VJoTIeQ3ud20tkkgvbOtzIf0Zs=
|
||||
github.com/libtnb/cache v1.2.0/go.mod h1:QzCzSS5KBP2dcwMZtwKZO94eFMOViYMkUuxeLallZTE=
|
||||
github.com/libtnb/chix v1.3.2 h1:LkA/+OaHeMyTiQoWEcCQKAMSKDZ0LxBXSENQO5d+OKk=
|
||||
github.com/libtnb/chix v1.3.2/go.mod h1:ItssStBa/ov7v4qujyYzORqIf7Z6DO7Q4carOM7pyAQ=
|
||||
github.com/libtnb/cron v0.2.0 h1:Dgn8kgooA+bYGC16OHnufGLQlvtFDmnoM0NVjyLOfrE=
|
||||
github.com/libtnb/cron v0.2.0/go.mod h1:nrOHxS4Ernkn1E//vs64xzC58hGfoL0+lYSW2zsROGI=
|
||||
github.com/libtnb/gormstore v1.2.0 h1:VdNTiG8Ah+glDFNxtj+ZrBhcqiLFxMviqlz2gez6zWU=
|
||||
github.com/libtnb/gormstore v1.2.0/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA=
|
||||
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b h1:73OlJdl4Zk6Ipnm7KKeE6yCjfd7nk0BhTArujx6WJCU=
|
||||
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b/go.mod h1:bBQRIiaGR9EZBC+x5hnwunNgcDOts7E1Oa7AowJwMFU=
|
||||
github.com/libtnb/securecookie v1.2.0 h1:2uc0PBDm0foeSTrcZ9QTX1IEjf6kFEwfgEYSIXQSKrA=
|
||||
github.com/libtnb/securecookie v1.2.0/go.mod h1:ja+wNGnQzYqcqXQnJWu6icsaWi5JEBwNEMJ2ReTVDxA=
|
||||
github.com/libtnb/sessions v1.4.0 h1:oQn4BckstpvqujSXejUcGAF4SQwTKFPIBe/Hv7s0aOs=
|
||||
github.com/libtnb/sessions v1.4.0/go.mod h1:PvxiaI3PCD+kzjPmIrpV3s9e4ACps6OUaXgmKCYlUFo=
|
||||
github.com/libtnb/sqlite v1.1.1 h1:p1Ud8vHTsPzgmChPUuJFkesrzl9ajD/pYrmoWf1cgig=
|
||||
github.com/libtnb/sqlite v1.1.1/go.mod h1:PNlcqMVWU+pMIUR499fHvRLUdxrNVVVXwEZkW1c9stE=
|
||||
github.com/libtnb/chix/v2 v2.1.1 h1:QQwHXUyPaUUp/KOwzXf2NB0IRKb9v2pqIdOIItzBPiE=
|
||||
github.com/libtnb/chix/v2 v2.1.1/go.mod h1:OEY9v0976U/IJzP6n8VdhgXH17VL2muo3EQWsIUyxm8=
|
||||
github.com/libtnb/cron v0.5.2 h1:Yy9a/S82bAS4KzE1SSMjNLtigPkejgMPUp9BHGeb1pw=
|
||||
github.com/libtnb/cron v0.5.2/go.mod h1:nrOHxS4Ernkn1E//vs64xzC58hGfoL0+lYSW2zsROGI=
|
||||
github.com/libtnb/gormstore v1.3.0 h1:OvVTYHZ9MCHI3Anf2ITtJLunAefyBQWG96EmHCE0eLM=
|
||||
github.com/libtnb/gormstore v1.3.0/go.mod h1:Z/n6VlVdLPmvunSM8sYGNHVebTTN5yUSTZsyNxW2IZc=
|
||||
github.com/libtnb/logrotate v0.1.2 h1:q9FLtYQYtzAlOB9bs91nPppqt3GNTUOonrBOivrtiOc=
|
||||
github.com/libtnb/logrotate v0.1.2/go.mod h1:yqC9N3C5mg0fBlquK+cWLB7xG5plQrP0ma84hMRj3ic=
|
||||
github.com/libtnb/securecookie v1.4.0 h1:SkKHO7T5I4aRGV7/6fnYYsleQDnnDzeAmTDA0GMPD98=
|
||||
github.com/libtnb/securecookie v1.4.0/go.mod h1:mg1i9HfstsYBGwCfQdU+3Z1GuieyZRAxbkFUnrzchJU=
|
||||
github.com/libtnb/sessions v1.5.0 h1:gvTRkSR8lpp1PB0Nms/kc+gh1BwJ411fxLjnZLYItLs=
|
||||
github.com/libtnb/sessions v1.5.0/go.mod h1:/Q/+lO7DcnJJEU1nL59ScPV/V8ct34F8BjoEMwwxjRw=
|
||||
github.com/libtnb/sqlite v1.2.1 h1:lrhN3yG49pj1gmtMMP6Es0lhKIkzDeqBul2EWOLyjpY=
|
||||
github.com/libtnb/sqlite v1.2.1/go.mod h1:pGq13BirCjkM6ihkWWxs0PGFhBiwUyA0PqdSmgDWfmM=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7 h1:ZzONsNhw69uC/wJQlMj66n1zfsRgKAElfJ//E9G380U=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/utils v1.2.1 h1:LJmReRREnpqfHyy9PZtNgBh3ZaIGct81b8ZaAsolMkM=
|
||||
github.com/libtnb/utils v1.2.1/go.mod h1:o6LEDeC42PXI21uLWdWJWTVYvR9BtAZfzzTGJVQoQiU=
|
||||
github.com/libtnb/utils v1.2.2 h1:HUqgAIb+TIrZPXRytYB5XqzkEXG2U4a4/ZGpprNtL5A=
|
||||
github.com/libtnb/utils v1.2.2/go.mod h1:KUCx2+Phw3cLXOwuQDy6XwFOliaYuU1rpAuJQuHf+lQ=
|
||||
github.com/libtnb/validator v0.4.1 h1:TYDC1C1yp93d02huzCqEc1aU/u5PAtC3jqGPtEdZchU=
|
||||
github.com/libtnb/validator v0.4.1/go.mod h1:j08Lydxwy7ModkoKeYIHxvZvAtnzzdE8xi2J+SGVrgc=
|
||||
github.com/libtnb/validator/contrib/openapi v0.2.0 h1:y47A16Lqtkc9/uifRIJ2XC+Rkgghj2fmHPrclcPXPtA=
|
||||
github.com/libtnb/validator/contrib/openapi v0.2.0/go.mod h1:2vCdRniTmXWBtG4gro9B7s6ko4v3WdoSKjvhsWdTn3g=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
@@ -296,13 +293,15 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/medama-io/go-useragent v1.2.3 h1:jTv5NI+dn2hAe6zlagfXe/Y4934/YPzqxvP/gP0DjCQ=
|
||||
github.com/medama-io/go-useragent v1.2.3/go.mod h1:H9GYWth4IN8vAFZh5LeARza7VwM4jK9uk7Tb9huVzLw=
|
||||
github.com/medama-io/go-useragent v1.2.4 h1:kDux3lCjZvBpccKus/goaZl6YMnrl9D8Tx/wb4HqRMI=
|
||||
github.com/medama-io/go-useragent v1.2.4/go.mod h1:H9GYWth4IN8vAFZh5LeARza7VwM4jK9uk7Tb9huVzLw=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
@@ -316,8 +315,10 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
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/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
@@ -337,9 +338,11 @@ github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJ
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
@@ -356,6 +359,10 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
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/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0=
|
||||
github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -365,12 +372,12 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
|
||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sethvargo/go-limiter v1.1.0 h1:eLeZVQ2zqJOiEs03GguqmBVG6/T6lsZB+6PP1t7J6fA=
|
||||
github.com/sethvargo/go-limiter v1.1.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||
github.com/shamaton/msgpack/v3 v3.0.0 h1:xl40uxWkSpwBCSTvS5wyXvJRsC6AcVcYeox9PspKiZg=
|
||||
github.com/shamaton/msgpack/v3 v3.0.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
|
||||
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/sethvargo/go-limiter v1.2.0 h1:XKL1vsaQ2zztVJrnZSzpRWCq/aLQqMllJ/3D/0bt/cw=
|
||||
github.com/sethvargo/go-limiter v1.2.0/go.mod h1:RC+qY2R7PAK81mBCrZEJlUlKnXSIqqQ8B7G44UgZ/1E=
|
||||
github.com/shamaton/msgpack/v3 v3.2.0 h1:1q2Ms+MWmuRju+PuDMSFDB7p7621npeX4zprJN5Zck8=
|
||||
github.com/shamaton/msgpack/v3 v3.2.0/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@@ -390,9 +397,11 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
|
||||
github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/studio-b12/gowebdav v0.13.0 h1:OcwSg6IQHOFNdYHn3bPOHwSE8looG8N56Y5xTT1asqQ=
|
||||
github.com/studio-b12/gowebdav v0.13.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
|
||||
github.com/timtadh/data-structures v0.6.1/go.mod h1:uYUnI1cQi/5yMCc7s23I+x8Mn8BCMf4WgK+7/4QSEk4=
|
||||
github.com/timtadh/data-structures v0.6.2 h1:zybDnU5NLjJ7WKMDJpvVwczQuf1wSLBgdRHZ9O4AqJ0=
|
||||
@@ -410,15 +419,23 @@ github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZ
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3 h1:ClWHRIz18WRVT3RD9y9uTwEWU9eEi+VYFEIumg05FLk=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3/go.mod h1:ALbEe81QPWOZjDKCKNWodG2iqCMtregG8+ebQgjx2+4=
|
||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
|
||||
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
|
||||
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
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/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88=
|
||||
github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
@@ -426,30 +443,46 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
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-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
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/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||
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=
|
||||
@@ -461,8 +494,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
|
||||
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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -477,8 +510,8 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
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=
|
||||
@@ -487,8 +520,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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=
|
||||
@@ -505,16 +538,16 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
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/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -534,8 +567,8 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
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-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=
|
||||
@@ -569,44 +602,46 @@ gopkg.in/yaml.v2 v2.2.1/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.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
|
||||
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
|
||||
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
|
||||
modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
resty.dev/v3 v3.0.0-beta.6 h1:ghRdNpoE8/wBCv+kTKIOauW1aCrSIeTq7GxtfYgtevU=
|
||||
resty.dev/v3 v3.0.0-beta.6/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
resty.dev/v3 v3.0.0-rc.3 h1:k24LZ03Cb4Ue5e6O/Pfxu5TQRBBYGES6wm2wceia+Io=
|
||||
resty.dev/v3 v3.0.0-rc.3/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
+2
-3
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/bddjr/hlfhr"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/gookit/validate"
|
||||
"github.com/libtnb/cron"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
@@ -31,7 +30,7 @@ type Ace struct {
|
||||
runner types.TaskRunner
|
||||
}
|
||||
|
||||
func NewAce(conf *config.Config, router *chi.Mux, server *hlfhr.Server, reloader *tlscert.Reloader, migrator *gormigrate.Gormigrate, cron *cron.Cron, runner types.TaskRunner, _ *validate.Validation) *Ace {
|
||||
func NewAce(router *chi.Mux, conf *config.Config, cron *cron.Cron, migrator *gormigrate.Gormigrate, server *hlfhr.Server, reloader *tlscert.Reloader, runner types.TaskRunner) (*Ace, error) {
|
||||
return &Ace{
|
||||
conf: conf,
|
||||
router: router,
|
||||
@@ -40,7 +39,7 @@ func NewAce(conf *config.Config, router *chi.Mux, server *hlfhr.Server, reloader
|
||||
migrator: migrator,
|
||||
cron: cron,
|
||||
runner: runner,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Ace) Run() error {
|
||||
|
||||
+4
-10
@@ -5,10 +5,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/gookit/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/apploader"
|
||||
)
|
||||
|
||||
type Cli struct {
|
||||
@@ -16,12 +13,12 @@ type Cli struct {
|
||||
migrator *gormigrate.Gormigrate
|
||||
}
|
||||
|
||||
func NewCli(cmd *cli.Command, migrator *gormigrate.Gormigrate, _ *apploader.Loader) *Cli {
|
||||
func NewCli(cmd *cli.Command, migrator *gormigrate.Gormigrate) (*Cli, error) {
|
||||
IsCli = true
|
||||
return &Cli{
|
||||
cmd: cmd,
|
||||
migrator: migrator,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Cli) Run() error {
|
||||
@@ -29,9 +26,6 @@ func (r *Cli) Run() error {
|
||||
// 这里不处理错误,这么做是为了在异常时用户可以用 fix 命令尝试修复
|
||||
_ = r.migrator.Migrate()
|
||||
|
||||
if err := r.cmd.Run(context.TODO(), os.Args); err != nil {
|
||||
color.Errorf("|-%v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
// 错误必须向上返回,调用方据此以非零码退出,后台任务才能正确判定失败
|
||||
return r.cmd.Run(context.TODO(), os.Args)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HealthLevelError 需要用户立即处理
|
||||
const HealthLevelError = "error"
|
||||
|
||||
// HealthLevelWarning 提示性问题,通常已被系统自动降级处理
|
||||
const HealthLevelWarning = "warning"
|
||||
|
||||
// HealthIssue 单条健康问题
|
||||
// Key 为稳定标识符(如 database:stat),前端据此选择翻译文案
|
||||
// Message 为原始错误详情,供诊断参考
|
||||
type HealthIssue struct {
|
||||
Key string `json:"key"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Since time.Time `json:"since"`
|
||||
}
|
||||
|
||||
type healthRegistry struct {
|
||||
mu sync.RWMutex
|
||||
issues map[string]HealthIssue
|
||||
}
|
||||
|
||||
// Health 全局健康状态注册表,供各后台任务上报/清除故障
|
||||
var Health = &healthRegistry{issues: make(map[string]HealthIssue)}
|
||||
|
||||
// Report 上报或更新一条健康问题
|
||||
// 同 key 重复上报时保留首次上报时间,仅更新 message 和 level
|
||||
func (h *healthRegistry) Report(key, level, message string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if existing, ok := h.issues[key]; ok {
|
||||
existing.Level = level
|
||||
existing.Message = message
|
||||
h.issues[key] = existing
|
||||
return
|
||||
}
|
||||
h.issues[key] = HealthIssue{
|
||||
Key: key,
|
||||
Level: level,
|
||||
Message: message,
|
||||
Since: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Clear 清除指定 key 的健康问题
|
||||
func (h *healthRegistry) Clear(key string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
delete(h.issues, key)
|
||||
}
|
||||
|
||||
// Snapshot 返回当前所有健康问题,按 Since 升序(越早发生越靠前)
|
||||
func (h *healthRegistry) Snapshot() []HealthIssue {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
result := make([]HealthIssue, 0, len(h.issues))
|
||||
for _, issue := range h.issues {
|
||||
result = append(result, issue)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Since.Before(result[j].Since)
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package acewaf
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(_ chi.Router) {
|
||||
// WAF 管理路由由 route 贡献(route/waf.go)统一提供
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("acewaf")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
@@ -22,10 +22,10 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
+13
-30
@@ -3,6 +3,7 @@ package apps
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/acewaf"
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pgadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
@@ -36,34 +38,15 @@ import (
|
||||
)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
apache.NewApp,
|
||||
clickhouse.NewApp,
|
||||
codeserver.NewApp,
|
||||
docker.NewApp,
|
||||
elasticsearch.NewApp,
|
||||
fail2ban.NewApp,
|
||||
frp.NewApp,
|
||||
gitea.NewApp,
|
||||
grafana.NewApp,
|
||||
kafka.NewApp,
|
||||
mariadb.NewApp,
|
||||
memcached.NewApp,
|
||||
minio.NewApp,
|
||||
mongodb.NewApp,
|
||||
mysql.NewApp,
|
||||
nginx.NewApp,
|
||||
openresty.NewApp,
|
||||
opensearch.NewApp,
|
||||
percona.NewApp,
|
||||
phpmyadmin.NewApp,
|
||||
podman.NewApp,
|
||||
postgresql.NewApp,
|
||||
prometheus.NewApp,
|
||||
pureftpd.NewApp,
|
||||
redis.NewApp,
|
||||
rocketmq.NewApp,
|
||||
rsync.NewApp,
|
||||
s3fs.NewApp,
|
||||
supervisor.NewApp,
|
||||
valkey.NewApp,
|
||||
apache.NewApp, clickhouse.NewApp, codeserver.NewApp,
|
||||
docker.NewApp, elasticsearch.NewApp, fail2ban.NewApp,
|
||||
frp.NewApp, gitea.NewApp, grafana.NewApp,
|
||||
kafka.NewApp, mariadb.NewApp, memcached.NewApp,
|
||||
minio.NewApp, mongodb.NewApp, mysql.NewApp,
|
||||
nginx.NewApp, openresty.NewApp, opensearch.NewApp,
|
||||
percona.NewApp, pgadmin.NewApp, phpmyadmin.NewApp, podman.NewApp,
|
||||
postgresql.NewApp, prometheus.NewApp, pureftpd.NewApp,
|
||||
redis.NewApp, rocketmq.NewApp, rsync.NewApp,
|
||||
s3fs.NewApp, supervisor.NewApp, valkey.NewApp,
|
||||
acewaf.NewApp,
|
||||
)
|
||||
|
||||
@@ -27,12 +27,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -66,7 +66,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 获取版本
|
||||
versionResp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+version()&user=default&password=%s", port, password))
|
||||
if err != nil || !versionResp.IsSuccess() {
|
||||
if err != nil || !versionResp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,21 +7,21 @@ type UpdateConfig struct {
|
||||
|
||||
// SetDefaultPassword 设置 default 用户密码
|
||||
type SetDefaultPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// ConfigTune ClickHouse 配置调整
|
||||
type ConfigTune struct {
|
||||
// 网络
|
||||
ListenHost string `form:"listen_host" json:"listen_host"`
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
TCPPort string `form:"tcp_port" json:"tcp_port"`
|
||||
HTTPPort string `form:"http_port" json:"http_port" validate:"number && min:1 && max:65535"`
|
||||
TCPPort string `form:"tcp_port" json:"tcp_port" validate:"number && min:1 && max:65535"`
|
||||
// 性能
|
||||
MaxMemoryUsage string `form:"max_memory_usage" json:"max_memory_usage"`
|
||||
MaxThreads string `form:"max_threads" json:"max_threads"`
|
||||
// 路径
|
||||
Path string `form:"path" json:"path"`
|
||||
TmpPath string `form:"tmp_path" json:"tmp_path"`
|
||||
Path string `form:"path" json:"path" validate:"unix_path"`
|
||||
TmpPath string `form:"tmp_path" json:"tmp_path" validate:"unix_path"`
|
||||
// 日志
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
LogLevel string `form:"log_level" json:"log_level" validate:"in:trace,debug,information,warning,error"`
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -12,22 +12,22 @@ type LogOpts struct {
|
||||
|
||||
// Settings Docker daemon 设置
|
||||
type Settings struct {
|
||||
RegistryMirrors []string `json:"registry-mirrors,omitempty"` // 注册表镜像
|
||||
InsecureRegistries []string `json:"insecure-registries,omitempty"` // 非安全镜像仓库
|
||||
LiveRestore bool `json:"live-restore,omitempty"` // Live restore
|
||||
LogDriver string `json:"log-driver,omitempty"` // 日志驱动
|
||||
LogOpts LogOpts `json:"log-opts"` // 日志配置选项
|
||||
CgroupDriver string `json:"cgroup-driver,omitempty"` // cgroup 驱动(从 exec-opts 中提取)
|
||||
Hosts []string `json:"hosts,omitempty"` // Socket 路径
|
||||
DataRoot string `json:"data-root,omitempty"` // 数据目录
|
||||
StorageDriver string `json:"storage-driver,omitempty"` // 存储驱动
|
||||
DNS []string `json:"dns,omitempty"` // DNS 配置
|
||||
FirewallBackend string `json:"firewall-backend,omitempty"` // 防火墙后端 (iptables/nftables)
|
||||
Iptables *bool `json:"iptables,omitempty"` // iptables 规则
|
||||
Ip6tables *bool `json:"ip6tables,omitempty"` // ip6tables 规则
|
||||
IpForward *bool `json:"ip-forward,omitempty"` // IP 转发
|
||||
IPv6 *bool `json:"ipv6,omitempty"` // IPv6 支持
|
||||
Bip string `json:"bip,omitempty"` // 默认 bridge 网络 IP 段
|
||||
RegistryMirrors []string `json:"registry-mirrors,omitempty"` // 注册表镜像
|
||||
InsecureRegistries []string `json:"insecure-registries,omitempty"` // 非安全镜像仓库
|
||||
LiveRestore bool `json:"live-restore,omitempty"` // Live restore
|
||||
LogDriver string `json:"log-driver,omitempty"` // 日志驱动
|
||||
LogOpts LogOpts `json:"log-opts"` // 日志配置选项
|
||||
CgroupDriver string `json:"cgroup-driver,omitempty" validate:"in:systemd,cgroupfs"` // cgroup 驱动(从 exec-opts 中提取)
|
||||
Hosts []string `json:"hosts,omitempty"` // Socket 路径
|
||||
DataRoot string `json:"data-root,omitempty"` // 数据目录
|
||||
StorageDriver string `json:"storage-driver,omitempty"` // 存储驱动
|
||||
DNS []string `json:"dns,omitempty"` // DNS 配置
|
||||
FirewallBackend string `json:"firewall-backend,omitempty" validate:"in:iptables,nftables"` // 防火墙后端 (iptables/nftables)
|
||||
Iptables *bool `json:"iptables,omitempty"` // iptables 规则
|
||||
Ip6tables *bool `json:"ip6tables,omitempty"` // ip6tables 规则
|
||||
IpForward *bool `json:"ip-forward,omitempty"` // IP 转发
|
||||
IPv6 *bool `json:"ipv6,omitempty"` // IPv6 支持
|
||||
Bip string `json:"bip,omitempty"` // 默认 bridge 网络 IP 段
|
||||
}
|
||||
|
||||
// UpdateSettings 更新设置请求
|
||||
|
||||
@@ -25,8 +25,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
return &App{t: t}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -57,7 +57,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/_cluster/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
@@ -27,11 +27,11 @@ type App struct {
|
||||
websiteRepo biz.WebsiteRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, website biz.WebsiteRepo) *App {
|
||||
func NewApp(t *gotext.Locale, websiteRepo biz.WebsiteRepo) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
websiteRepo: website,
|
||||
}
|
||||
websiteRepo: websiteRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -177,7 +177,7 @@ ignoreregex =
|
||||
switch jailName {
|
||||
case "ssh":
|
||||
filter = "sshd"
|
||||
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}' | paste -sd ','")
|
||||
case "mysql":
|
||||
filter = "mysqld-auth"
|
||||
port, err = shell.Execf("cat %s/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'", app.Root)
|
||||
|
||||
@@ -2,12 +2,12 @@ package fail2ban
|
||||
|
||||
type Add struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
MaxRetry int `json:"maxretry" validate:"required"`
|
||||
FindTime int `json:"findtime" validate:"required"`
|
||||
BanTime int `json:"bantime" validate:"required"`
|
||||
WebsiteName string `json:"website_name"`
|
||||
WebsiteMode string `json:"website_mode"`
|
||||
Type string `json:"type" validate:"required && in:service,website"`
|
||||
MaxRetry int `json:"maxretry" validate:"required && min:1"`
|
||||
FindTime int `json:"findtime" validate:"required && min:1"`
|
||||
BanTime int `json:"bantime" validate:"required && min:1"`
|
||||
WebsiteName string `json:"website_name" validate:"required_if:Type,website"`
|
||||
WebsiteMode string `json:"website_mode" validate:"required_if:Type,website && in:cc,path"`
|
||||
WebsitePath string `json:"website_path"`
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ type BanList struct {
|
||||
|
||||
type Unban struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IP string `json:"ip" validate:"required"`
|
||||
IP string `json:"ip" validate:"required && ip"`
|
||||
}
|
||||
|
||||
type SetWhiteList struct {
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package frp
|
||||
|
||||
type Name struct {
|
||||
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
|
||||
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
|
||||
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateUser struct {
|
||||
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
|
||||
User string `form:"user" json:"user" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Group string `form:"group" json:"group" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
|
||||
User string `form:"user" json:"user" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
|
||||
Group string `form:"group" json:"group" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -24,8 +24,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
return &App{t: t}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -67,7 +67,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/api/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ type ConfigTune struct {
|
||||
// DataSource 数据源
|
||||
type DataSource struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:prometheus,mysql,postgres,influxdb,loki,elasticsearch,tempo,jaeger,zipkin,graphite,alertmanager,opentsdb,mssql,testdata,grafana-pyroscope-datasource,grafana-opensearch-datasource"`
|
||||
Type string `form:"type" json:"type" validate:"required && in:prometheus,mysql,postgres,influxdb,loki,elasticsearch,tempo,jaeger,zipkin,graphite,alertmanager,opentsdb,mssql,testdata,grafana-pyroscope-datasource,grafana-opensearch-datasource"`
|
||||
URL string `form:"url" json:"url" validate:"required"`
|
||||
Access string `form:"access" json:"access"`
|
||||
Access string `form:"access" json:"access" validate:"in:proxy,direct"`
|
||||
IsDefault bool `form:"is_default" json:"is_default"`
|
||||
// 数据库类型专用
|
||||
Database string `form:"database" json:"database"`
|
||||
|
||||
@@ -20,8 +20,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
return &App{t: t}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -2,20 +2,18 @@ package mariadb
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
mysql *mysql.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(mysqlApp *mysql.App) (*App, error) {
|
||||
return &App{
|
||||
mysql: mysql.NewApp(t, setting, databaseServer),
|
||||
}
|
||||
mysql: mysqlApp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -20,10 +20,10 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -6,10 +6,10 @@ type UpdateConfig struct {
|
||||
|
||||
// ConfigTune Memcached 配置调整
|
||||
type ConfigTune struct {
|
||||
Port string `form:"port" json:"port"`
|
||||
UDPPort string `form:"udp_port" json:"udp_port"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
UDPPort string `form:"udp_port" json:"udp_port" validate:"number && min:1 && max:65535"`
|
||||
ListenAddress string `form:"listen_address" json:"listen_address"`
|
||||
Memory string `form:"memory" json:"memory"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
Threads string `form:"threads" json:"threads"`
|
||||
Memory string `form:"memory" json:"memory" validate:"number && min:1"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections" validate:"number && min:1"`
|
||||
Threads string `form:"threads" json:"threads" validate:"number && min:1"`
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -26,12 +26,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -7,19 +7,19 @@ type UpdateConfig struct {
|
||||
|
||||
// SetAdminPassword 设置 admin 密码
|
||||
type SetAdminPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// ConfigTune MongoDB 配置调整
|
||||
type ConfigTune struct {
|
||||
// 存储
|
||||
DbPath string `form:"db_path" json:"db_path"`
|
||||
DbPath string `form:"db_path" json:"db_path" validate:"unix_path"`
|
||||
CacheSizeGB string `form:"cache_size_gb" json:"cache_size_gb"`
|
||||
// 网络
|
||||
Port string `form:"port" json:"port"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
BindIp string `form:"bind_ip" json:"bind_ip"`
|
||||
// 日志
|
||||
SystemLogPath string `form:"system_log_path" json:"system_log_path"`
|
||||
SystemLogPath string `form:"system_log_path" json:"system_log_path" validate:"unix_path"`
|
||||
// 安全
|
||||
Authorization string `form:"authorization" json:"authorization"`
|
||||
Authorization string `form:"authorization" json:"authorization" validate:"in:enabled,disabled"`
|
||||
}
|
||||
|
||||
+12
-29
@@ -28,12 +28,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -222,7 +222,7 @@ func (s *App) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
oldRootPassword, _ := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
mysql, err := db.NewMySQL("root", oldRootPassword, s.getSock(), "unix")
|
||||
mysql, err := db.NewMySQL(r.Context(), "root", oldRootPassword, s.getSock(), "unix")
|
||||
if err != nil {
|
||||
// 尝试安全模式直接改密
|
||||
if err = db.MySQLResetRootPassword(req.Password); err != nil {
|
||||
@@ -247,30 +247,6 @@ func (s *App) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) getSock() string {
|
||||
if io.Exists("/tmp/mysql.sock") {
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
if io.Exists(app.Root + "/server/mysql/config/my.cnf") {
|
||||
config, _ := io.Read(app.Root + "/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"
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 MySQL 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
@@ -369,6 +345,13 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) getSock() string {
|
||||
if sock := db.MySQLSocket(app.Root+"/server/mysql/config/my.cnf", "/etc/my.cnf"); sock != "" {
|
||||
return sock
|
||||
}
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 格式内容中获取指定键的值
|
||||
func (s *App) getINIValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
|
||||
@@ -5,13 +5,13 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type SetRootPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// ConfigTune MySQL 配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Port string `form:"port" json:"port"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
MaxConnectErrors string `form:"max_connect_errors" json:"max_connect_errors"`
|
||||
DefaultStorageEngine string `form:"default_storage_engine" json:"default_storage_engine"`
|
||||
@@ -32,7 +32,7 @@ type ConfigTune struct {
|
||||
// InnoDB
|
||||
InnodbBufferPoolSize string `form:"innodb_buffer_pool_size" json:"innodb_buffer_pool_size"`
|
||||
InnodbLogBufferSize string `form:"innodb_log_buffer_size" json:"innodb_log_buffer_size"`
|
||||
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit"`
|
||||
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit" validate:"in:0,1,2"`
|
||||
InnodbLockWaitTimeout string `form:"innodb_lock_wait_timeout" json:"innodb_lock_wait_timeout"`
|
||||
InnodbMaxDirtyPagesPct string `form:"innodb_max_dirty_pages_pct" json:"innodb_max_dirty_pages_pct"`
|
||||
InnodbReadIoThreads string `form:"innodb_read_io_threads" json:"innodb_read_io_threads"`
|
||||
|
||||
@@ -25,10 +25,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -103,7 +104,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,22 +39,22 @@ type ConfigTune struct {
|
||||
}
|
||||
|
||||
type StreamServer struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 配置名称,用于文件命名
|
||||
Listen string `form:"listen" json:"listen" validate:"required"` // 监听地址,如: "12345", "0.0.0.0:12345", "[::]:12345"
|
||||
UDP bool `form:"udp" json:"udp"` // 是否 UDP 协议
|
||||
ProxyPass string `form:"proxy_pass" json:"proxy_pass" validate:"required"` // 代理地址,如: "127.0.0.1:3306", "upstream_name"
|
||||
ProxyProtocol bool `form:"proxy_protocol" json:"proxy_protocol"` // 是否启用 PROXY 协议
|
||||
ProxyTimeout time.Duration `form:"proxy_timeout" json:"proxy_timeout"` // 代理超时时间
|
||||
ProxyConnectTimeout time.Duration `form:"proxy_connect_timeout" json:"proxy_connect_timeout"` // 代理连接超时时间
|
||||
SSL bool `form:"ssl" json:"ssl"` // 是否启用 SSL
|
||||
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"requiredIf:SSL,true"` // SSL 证书路径
|
||||
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"requiredIf:SSL,true"` // SSL 私钥路径
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""` // 配置名称,用于文件命名
|
||||
Listen string `form:"listen" json:"listen" validate:"required"` // 监听地址,如: "12345", "0.0.0.0:12345", "[::]:12345"
|
||||
UDP bool `form:"udp" json:"udp"` // 是否 UDP 协议
|
||||
ProxyPass string `form:"proxy_pass" json:"proxy_pass" validate:"required"` // 代理地址,如: "127.0.0.1:3306", "upstream_name"
|
||||
ProxyProtocol bool `form:"proxy_protocol" json:"proxy_protocol"` // 是否启用 PROXY 协议
|
||||
ProxyTimeout time.Duration `form:"proxy_timeout" json:"proxy_timeout"` // 代理超时时间
|
||||
ProxyConnectTimeout time.Duration `form:"proxy_connect_timeout" json:"proxy_connect_timeout"` // 代理连接超时时间
|
||||
SSL bool `form:"ssl" json:"ssl"` // 是否启用 SSL
|
||||
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"required_if:SSL,true"` // SSL 证书路径
|
||||
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"required_if:SSL,true"` // SSL 私钥路径
|
||||
}
|
||||
|
||||
type StreamUpstream struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 上游名称
|
||||
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["127.0.0.1:3306"] = "weight=5"
|
||||
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "hash $remote_addr"
|
||||
Resolver []string `form:"resolver" json:"resolver"` // DNS 解析器,如: ["8.8.8.8", "ipv6=off"]
|
||||
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""` // 上游名称
|
||||
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["127.0.0.1:3306"] = "weight=5"
|
||||
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "hash $remote_addr"
|
||||
Resolver []string `form:"resolver" json:"resolver"` // DNS 解析器,如: ["8.8.8.8", "ipv6=off"]
|
||||
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package openresty
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -13,10 +12,10 @@ type App struct {
|
||||
nginx *nginx.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(nginxApp *nginx.App) (*App, error) {
|
||||
return &App{
|
||||
nginx: nginx.NewApp(t),
|
||||
}
|
||||
nginx: nginxApp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -24,6 +23,6 @@ func (s *App) Route(r chi.Router) {
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("openresty")
|
||||
ok, _ := systemctl.Status("nginx")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
|
||||
return &App{t: t}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -57,7 +58,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/_cluster/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,20 +2,18 @@ package percona
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
mysql *mysql.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(mysqlApp *mysql.App) (*App, error) {
|
||||
return &App{
|
||||
mysql: mysql.NewApp(t, setting, databaseServer),
|
||||
}
|
||||
mysql: mysqlApp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
package pgadmin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
stdio "io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
conf: conf,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// pgadminLanguage 面板语言映射为 pgAdmin 语言代码
|
||||
func (s *App) pgadminLanguage() string {
|
||||
switch s.conf.App.Locale {
|
||||
case "":
|
||||
return ""
|
||||
case "zh_CN":
|
||||
return "zh_Hans_CN"
|
||||
case "zh_TW":
|
||||
return "zh_Hant_TW"
|
||||
default:
|
||||
return s.conf.App.Locale
|
||||
}
|
||||
}
|
||||
|
||||
// clientIP 获取请求来源 IP,面板位于反代之后时优先取配置的 IP 头
|
||||
func (s *App) clientIP(r *http.Request) string {
|
||||
ip := r.RemoteAddr
|
||||
if header := s.conf.HTTP.IPHeader; header != "" && r.Header.Get(header) != "" {
|
||||
ip = strings.TrimSpace(strings.Split(r.Header.Get(header), ",")[0])
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
ip = host
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/info", s.Info)
|
||||
r.Post("/port", s.UpdatePort)
|
||||
r.Post("/login", s.Login)
|
||||
r.Post("/update_username", s.UpdateUsername)
|
||||
r.Post("/reset_password", s.ResetPassword)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("pgadmin")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) path() string {
|
||||
return fmt.Sprintf("%s/server/pgadmin", app.Root)
|
||||
}
|
||||
|
||||
// port 从 systemd 环境文件中解析监听端口
|
||||
func (s *App) port() (uint, error) {
|
||||
conf, err := io.Read(fmt.Sprintf("%s/pgadmin.conf", s.path()))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
match := regexp.MustCompile(`PGADMIN_LISTEN=.+:(\d+)`).FindStringSubmatch(conf)
|
||||
if len(match) < 2 {
|
||||
return 0, errors.New(s.t.Get("pgAdmin port not found"))
|
||||
}
|
||||
|
||||
return cast.ToUint(match[1]), nil
|
||||
}
|
||||
|
||||
// credential 读取安装时生成的初始凭据(邮箱与密码)
|
||||
func (s *App) credential() (string, string) {
|
||||
raw, err := io.Read(fmt.Sprintf("%s/credential", s.path()))
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(raw), "\n")
|
||||
if len(lines) < 2 {
|
||||
return strings.TrimSpace(lines[0]), ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(lines[0]), strings.TrimSpace(lines[1])
|
||||
}
|
||||
|
||||
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
port, err := s.port()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
email, password := s.credential()
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"port": port,
|
||||
"email": email,
|
||||
"password": password,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdatePort](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
conf := fmt.Sprintf("%s/pgadmin.conf", s.path())
|
||||
content, err := io.Read(conf)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
content = regexp.MustCompile(`PGADMIN_LISTEN=(.+):\d+`).ReplaceAllString(content, "PGADMIN_LISTEN=${1}:"+cast.ToString(req.Port))
|
||||
if err = io.Write(conf, content, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("pgadmin"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// serversFile pgAdmin dump-servers/load-servers 的 JSON 结构
|
||||
type serversFile struct {
|
||||
Servers map[string]serverEntry `json:"Servers"`
|
||||
}
|
||||
|
||||
type serverEntry struct {
|
||||
Name string `json:"Name"`
|
||||
Group string `json:"Group"`
|
||||
Host string `json:"Host"`
|
||||
Port int `json:"Port"`
|
||||
MaintenanceDB string `json:"MaintenanceDB"`
|
||||
Username string `json:"Username"`
|
||||
SSLMode string `json:"SSLMode,omitempty"`
|
||||
PassFile string `json:"PassFile,omitempty"`
|
||||
}
|
||||
|
||||
// escapePgpass 转义 pgpass 字段中的反斜杠与冒号
|
||||
func escapePgpass(s string) string {
|
||||
return strings.NewReplacer(`\`, `\\`, `:`, `\:`).Replace(s)
|
||||
}
|
||||
|
||||
// existingServers 只读 pgAdmin 配置库查询 Servers 组内已注册的服务器
|
||||
// CLI 每次调用都要冷启动整个 pgAdmin 应用,直读库快数个量级
|
||||
func (s *App) existingServers(email string) (map[string]struct{}, error) {
|
||||
db, err := sql.Open("sqlite", fmt.Sprintf("file:%s/data/pgadmin.db?mode=ro", s.path()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
rows, err := db.Query(`SELECT s.host, s.port, s.username FROM server s
|
||||
JOIN servergroup g ON s.servergroup_id = g.id
|
||||
JOIN "user" u ON s.user_id = u.id
|
||||
WHERE u.email = ? AND g.name = 'Servers'`, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
existing := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var host, username sql.NullString
|
||||
var port sql.NullInt64
|
||||
if err = rows.Scan(&host, &port, &username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existing[fmt.Sprintf("%s:%d:%s", host.String, port.Int64, username.String)] = struct{}{}
|
||||
}
|
||||
return existing, rows.Err()
|
||||
}
|
||||
|
||||
// dumpExistingServers 通过 CLI 导出查询已注册服务器,直读配置库失败时的回退路径
|
||||
func (s *App) dumpExistingServers(email string) map[string]struct{} {
|
||||
dump := filepath.Join(os.TempDir(), "pgadmin-servers.json")
|
||||
defer func() { _ = io.Remove(dump) }()
|
||||
_, _ = shell.Execf("%s/cli dump-servers '%s' --user '%s'", s.path(), dump, email)
|
||||
|
||||
existing := make(map[string]struct{})
|
||||
if raw, err := io.Read(dump); err == nil {
|
||||
var dumped serversFile
|
||||
if err = json.Unmarshal([]byte(raw), &dumped); err == nil {
|
||||
for _, item := range dumped.Servers {
|
||||
// 只认默认 Servers 组内的条目,历史版本同步进其他分组的会在此组补齐
|
||||
if item.Group != "Servers" {
|
||||
continue
|
||||
}
|
||||
existing[fmt.Sprintf("%s:%d:%s", item.Host, item.Port, item.Username)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
// syncServers 将面板中全部 PostgreSQL 服务器合并注册到 pgAdmin,凭据写入 pgpass 实现免密
|
||||
// 仅追加 pgAdmin 中缺失的服务器,不影响用户在 pgAdmin 中手动添加的内容
|
||||
func (s *App) syncServers(ctx context.Context, email string) error {
|
||||
servers, _, err := s.databaseServerRepo.List(ctx, 1, 10000, string(biz.DatabaseTypePostgresql))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// pgAdmin server 模式下 PassFile 以用户 storage 目录为根,目录名为邮箱 @ 转 _
|
||||
storageDir := fmt.Sprintf("%s/data/storage/%s", s.path(), strings.ReplaceAll(email, "@", "_"))
|
||||
pgpass := filepath.Join(storageDir, "pgpass")
|
||||
|
||||
// 重写 pgpass 中面板服务器的凭据行,保留其他行
|
||||
prefixes := make([]string, 0, len(servers))
|
||||
entries := make([]string, 0, len(servers))
|
||||
for _, server := range servers {
|
||||
prefix := fmt.Sprintf("%s:%d:*:%s:", escapePgpass(server.Host), server.Port, escapePgpass(server.Username))
|
||||
prefixes = append(prefixes, prefix)
|
||||
entries = append(entries, prefix+escapePgpass(server.Password))
|
||||
}
|
||||
var lines []string
|
||||
if raw, err := io.Read(pgpass); err == nil {
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(raw), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
panelOwned := false
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
panelOwned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !panelOwned {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
lines = append(lines, entries...)
|
||||
if err = os.MkdirAll(storageDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(pgpass, strings.Join(lines, "\n")+"\n", 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 查询 pgAdmin 已有服务器用于查缺,直读配置库,异常时回退 CLI 导出
|
||||
existing, err := s.existingServers(email)
|
||||
if err != nil {
|
||||
existing = s.dumpExistingServers(email)
|
||||
}
|
||||
|
||||
// 一次性合并导入缺失的服务器
|
||||
missing := make(map[string]serverEntry)
|
||||
for i, server := range servers {
|
||||
if _, ok := existing[fmt.Sprintf("%s:%d:%s", server.Host, server.Port, server.Username)]; ok {
|
||||
continue
|
||||
}
|
||||
missing[cast.ToString(i+1)] = serverEntry{
|
||||
Name: server.Name,
|
||||
Group: "Servers",
|
||||
Host: server.Host,
|
||||
Port: int(server.Port),
|
||||
MaintenanceDB: "postgres",
|
||||
Username: server.Username,
|
||||
SSLMode: "prefer",
|
||||
PassFile: "/pgpass",
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
load := filepath.Join(os.TempDir(), "pgadmin-servers-add.json")
|
||||
defer func() { _ = io.Remove(load) }()
|
||||
payload, err := json.Marshal(serversFile{Servers: missing})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(load, string(payload), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if out, err := shell.Execf("%s/cli load-servers '%s' --user '%s'", s.path(), load, email); err != nil {
|
||||
return errors.Join(err, errors.New(out))
|
||||
}
|
||||
// CLI 以 root 运行,修正数据目录属主避免服务写入失败
|
||||
if _, err = shell.Execf("chown -R www:www %s/data", s.path()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 常态路径仅写入了 pgpass,精确修正属主即可
|
||||
if err = io.Chown(storageDir, "www", "www"); err != nil {
|
||||
return err
|
||||
}
|
||||
return io.Chown(pgpass, "www", "www")
|
||||
}
|
||||
|
||||
// Login 同步面板全部 PostgreSQL 服务器后代理登录 pgAdmin 并将会话 Cookie 转发给浏览器
|
||||
// 面板与 pgAdmin 同主机不同端口,Cookie 按主机共享,浏览器凭转发的 Cookie 即为已登录态
|
||||
func (s *App) Login(w http.ResponseWriter, r *http.Request) {
|
||||
port, err := s.port()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
email, password := s.credential()
|
||||
if email == "" || password == "" {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("pgAdmin credential file not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.syncServers(r.Context(), email); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to sync servers to pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// 透传浏览器 IP 与 UA,pgAdmin 增强 Cookie 保护将会话绑定到 sha256(IP|UA),
|
||||
// 伪装成浏览器身份登录后浏览器直连即可通过校验,无需关闭该保护
|
||||
clientIP := s.clientIP(r)
|
||||
clientUA := r.UserAgent()
|
||||
|
||||
// 获取登录页以取得会话 Cookie 与 CSRF token
|
||||
loginURL := fmt.Sprintf("http://127.0.0.1:%d/login", port)
|
||||
pageReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, loginURL, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
pageReq.Header.Set("User-Agent", clientUA)
|
||||
pageReq.Header.Set("X-Forwarded-For", clientIP)
|
||||
pageResp, err := client.Do(pageReq)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = pageResp.Body.Close() }()
|
||||
page, err := stdio.ReadAll(stdio.LimitReader(pageResp.Body, 4<<20))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 登录页为 React 渲染,CSRF token 在内嵌 JSON 中,保留 input 形态兼容旧版本
|
||||
csrf := regexp.MustCompile(`"csrfToken":\s*"([^"]+)"`).FindStringSubmatch(string(page))
|
||||
if len(csrf) < 2 {
|
||||
csrf = regexp.MustCompile(`name="csrf_token"[^>]*value="([^"]+)"`).FindStringSubmatch(string(page))
|
||||
}
|
||||
if len(csrf) < 2 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse pgAdmin login page"))
|
||||
return
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("csrf_token", csrf[1])
|
||||
form.Set("email", email)
|
||||
form.Set("password", password)
|
||||
// 语言跟随面板设置,pgAdmin 会将 language 字段固化进会话
|
||||
if lang := s.pgadminLanguage(); lang != "" {
|
||||
form.Set("language", lang)
|
||||
}
|
||||
|
||||
loginReq, err := http.NewRequestWithContext(r.Context(), http.MethodPost, fmt.Sprintf("http://127.0.0.1:%d/authenticate/login", port), strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
loginReq.Header.Set("User-Agent", clientUA)
|
||||
loginReq.Header.Set("X-Forwarded-For", clientIP)
|
||||
for _, cookie := range pageResp.Cookies() {
|
||||
loginReq.AddCookie(cookie)
|
||||
}
|
||||
|
||||
loginResp, err := client.Do(loginReq)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = loginResp.Body.Close() }()
|
||||
_, _ = stdio.Copy(stdio.Discard, stdio.LimitReader(loginResp.Body, 4<<20))
|
||||
|
||||
// 登录成功时 pgAdmin 返回 302 且跳转目标不是登录页
|
||||
location := loginResp.Header.Get("Location")
|
||||
if loginResp.StatusCode != http.StatusFound || strings.Contains(location, "/login") {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to login pgAdmin, please check the credential of pgAdmin"))
|
||||
return
|
||||
}
|
||||
|
||||
// 改写 SameSite 以兼容面板 https 跳转 http 的场景
|
||||
for _, cookie := range append(pageResp.Cookies(), loginResp.Cookies()...) {
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
cookie.Secure = false
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"port": port,
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword 通过 CLI 重置管理员密码并同步凭据文件
|
||||
// UpdateUsername 修改管理员账号,迁移服务器配置与用户存储目录
|
||||
func (s *App) UpdateUsername(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateUsername](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
oldEmail, password := s.credential()
|
||||
if oldEmail == "" || password == "" {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("pgAdmin credential file not found"))
|
||||
return
|
||||
}
|
||||
if req.Username == oldEmail {
|
||||
service.Success(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 以当前密码创建新管理员账号
|
||||
if out, err := shell.Execf("%s/cli add-user '%s' '%s' --admin", s.path(), req.Username, password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to create new account: %v", errors.Join(err, errors.New(out))))
|
||||
return
|
||||
}
|
||||
|
||||
// 迁移服务器连接配置到新账号
|
||||
dump := filepath.Join(os.TempDir(), "pgadmin-servers-migrate.json")
|
||||
defer func() { _ = io.Remove(dump) }()
|
||||
_, _ = shell.Execf("%s/cli dump-servers '%s' --user '%s'", s.path(), dump, oldEmail)
|
||||
if io.Exists(dump) {
|
||||
_, _ = shell.Execf("%s/cli load-servers '%s' --user '%s'", s.path(), dump, req.Username)
|
||||
}
|
||||
|
||||
// 删除旧账号
|
||||
if out, err := shell.Execf("%s/cli delete-user '%s' --yes", s.path(), oldEmail); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to delete old account: %v", errors.Join(err, errors.New(out))))
|
||||
return
|
||||
}
|
||||
|
||||
// 迁移用户存储目录(pgpass 等),目录名为邮箱 @ 转 _
|
||||
oldDir := fmt.Sprintf("%s/data/storage/%s", s.path(), strings.ReplaceAll(oldEmail, "@", "_"))
|
||||
newDir := fmt.Sprintf("%s/data/storage/%s", s.path(), strings.ReplaceAll(req.Username, "@", "_"))
|
||||
if io.Exists(oldDir) && !io.Exists(newDir) {
|
||||
_ = os.Rename(oldDir, newDir)
|
||||
}
|
||||
|
||||
// CLI 以 root 运行,修正数据目录属主避免服务写入失败
|
||||
if _, err = shell.Execf("chown -R www:www %s/data", s.path()); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新凭据文件,下次登录按新账号重新同步
|
||||
if err = io.Write(fmt.Sprintf("%s/credential", s.path()), req.Username+"\n"+password+"\n", 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ResetPassword](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
email, _ := s.credential()
|
||||
if email == "" {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("pgAdmin credential file not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// cli 为安装脚本生成的稳定入口,屏蔽上游命令名随大版本变化
|
||||
if out, err := shell.Execf("%s/cli update-user '%s' --password '%s'", s.path(), email, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reset password: %v", errors.Join(err, errors.New(out))))
|
||||
return
|
||||
}
|
||||
// CLI 以 root 运行,修正数据目录属主避免服务写入失败
|
||||
if _, err = shell.Execf("chown -R www:www %s/data", s.path()); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/credential", s.path()), email+"\n"+req.Password+"\n", 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package pgadmin
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required && number && min:1 && max:65535"`
|
||||
}
|
||||
|
||||
type ResetPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
type UpdateUsername struct {
|
||||
Username string `form:"username" json:"username" validate:"required && email"`
|
||||
}
|
||||
+180
-16
@@ -1,19 +1,27 @@
|
||||
package phpmyadmin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
stdio "io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
@@ -22,31 +30,38 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
t: t,
|
||||
conf: conf,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/info", s.Info)
|
||||
r.Post("/port", s.UpdatePort)
|
||||
r.Post("/login", s.Login)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
// Status phpMyAdmin 由 nginx 站点承载,运行状态与 nginx 一致
|
||||
func (s *App) Status() string {
|
||||
return types.AppStatusNA
|
||||
ok, _ := systemctl.Status("nginx")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
// info 获取 phpMyAdmin 的访问目录与端口
|
||||
func (s *App) info() (string, int, error) {
|
||||
files, err := os.ReadDir(fmt.Sprintf("%s/server/phpmyadmin", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin directory not found"))
|
||||
return
|
||||
return "", 0, errors.New(s.t.Get("phpMyAdmin directory not found"))
|
||||
}
|
||||
|
||||
var phpmyadmin string
|
||||
@@ -56,24 +71,173 @@ func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if len(phpmyadmin) == 0 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin directory not found"))
|
||||
return
|
||||
return "", 0, errors.New(s.t.Get("phpMyAdmin directory not found"))
|
||||
}
|
||||
|
||||
conf, err := io.Read(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
return "", 0, err
|
||||
}
|
||||
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
|
||||
if len(match) == 0 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin port not found"))
|
||||
return "", 0, errors.New(s.t.Get("phpMyAdmin port not found"))
|
||||
}
|
||||
|
||||
return phpmyadmin, cast.ToInt(match[1]), nil
|
||||
}
|
||||
|
||||
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
path, port, err := s.info()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"path": phpmyadmin,
|
||||
"port": cast.ToInt(match[1]),
|
||||
"path": path,
|
||||
"port": port,
|
||||
})
|
||||
}
|
||||
|
||||
// ensureConfig 为存量安装补写 config.inc.php,允许登录到任意 MySQL 服务器
|
||||
func (s *App) ensureConfig(path string) error {
|
||||
config := fmt.Sprintf("%s/server/phpmyadmin/%s/config.inc.php", app.Root, path)
|
||||
if io.Exists(config) {
|
||||
return nil
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(`<?php
|
||||
declare(strict_types=1);
|
||||
$cfg['blowfish_secret'] = '%s';
|
||||
$cfg['AllowArbitraryServer'] = true;
|
||||
`, str.Random(32))
|
||||
return io.Write(config, content, 0644)
|
||||
}
|
||||
|
||||
// Login 代理登录 phpMyAdmin 并将会话 Cookie 转发给浏览器
|
||||
// 面板与 phpMyAdmin 同主机不同端口,Cookie 按主机共享,浏览器凭转发的 Cookie 即为已登录态
|
||||
func (s *App) Login(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Login](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
server, err := s.databaseServerRepo.Get(r.Context(), req.ServerID)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if server.Type != biz.DatabaseTypeMysql {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("server %s is not a MySQL server", server.Name))
|
||||
return
|
||||
}
|
||||
|
||||
path, port, err := s.info()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.ensureConfig(path); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
loginURL := fmt.Sprintf("http://127.0.0.1:%d/%s/index.php?route=/", port, path)
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// 获取登录页以取得会话 Cookie 与 CSRF token
|
||||
// 转发浏览器语言,避免语言协商落空后回落英文
|
||||
pageReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, loginURL, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
pageReq.Header.Set("Accept-Language", r.Header.Get("Accept-Language"))
|
||||
pageResp, err := client.Do(pageReq)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request phpMyAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = pageResp.Body.Close() }()
|
||||
page, err := stdio.ReadAll(stdio.LimitReader(pageResp.Body, 4<<20))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request phpMyAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
token := regexp.MustCompile(`name="token" value="([^"]+)"`).FindStringSubmatch(string(page))
|
||||
session := regexp.MustCompile(`name="set_session" value="([^"]+)"`).FindStringSubmatch(string(page))
|
||||
if len(token) < 2 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse phpMyAdmin login page"))
|
||||
return
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("route", "/")
|
||||
// 语言跟随面板设置,登录后固化进 phpMyAdmin 会话
|
||||
if s.conf.App.Locale != "" {
|
||||
form.Set("lang", s.conf.App.Locale)
|
||||
}
|
||||
form.Set("token", html.UnescapeString(token[1]))
|
||||
if len(session) >= 2 {
|
||||
form.Set("set_session", html.UnescapeString(session[1]))
|
||||
}
|
||||
form.Set("pma_username", server.Username)
|
||||
form.Set("pma_password", server.Password)
|
||||
form.Set("server", "1")
|
||||
// 本地默认端口走 phpMyAdmin 默认配置(socket 连接),其余场景显式指定目标服务器
|
||||
// host 为 localhost 但端口非默认时须用 127.0.0.1 强制走 TCP,否则 mysqli 会忽略端口走 socket
|
||||
isLocal := server.Host == "localhost" || server.Host == "127.0.0.1"
|
||||
if !isLocal || server.Port != 3306 {
|
||||
host := server.Host
|
||||
if host == "localhost" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
form.Set("pma_servername", fmt.Sprintf("%s %d", host, server.Port))
|
||||
}
|
||||
|
||||
loginReq, err := http.NewRequestWithContext(r.Context(), http.MethodPost, loginURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
loginReq.Header.Set("Accept-Language", r.Header.Get("Accept-Language"))
|
||||
for _, cookie := range pageResp.Cookies() {
|
||||
loginReq.AddCookie(cookie)
|
||||
}
|
||||
|
||||
loginResp, err := client.Do(loginReq)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request phpMyAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = loginResp.Body.Close() }()
|
||||
_, _ = stdio.Copy(stdio.Discard, stdio.LimitReader(loginResp.Body, 4<<20))
|
||||
|
||||
// 登录成功时 phpMyAdmin 返回 302 并携带会话 Cookie
|
||||
if loginResp.StatusCode != http.StatusFound {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to login phpMyAdmin, please check the credentials and status of server %s", server.Name))
|
||||
return
|
||||
}
|
||||
|
||||
// 改写 SameSite 以兼容面板 https 跳转 http 的场景
|
||||
for _, cookie := range append(pageResp.Cookies(), loginResp.Cookies()...) {
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
cookie.Secure = false
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"path": path,
|
||||
"port": port,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,5 +5,9 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"`
|
||||
Port uint `form:"port" json:"port" validate:"required && number && min:1 && max:65535"`
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -26,12 +27,15 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) (*App, error) {
|
||||
|
||||
setting := settingRepo
|
||||
databaseServer := databaseServerRepo
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -55,7 +59,7 @@ func (s *App) Status() string {
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取配置
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root))
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -72,13 +76,14 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root), req.Config, 0644); err != nil {
|
||||
oldPort := s.getPort()
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("postgresql"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload PostgreSQL: %v", err))
|
||||
if err = s.applyConfig(req.Config, oldPort); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to apply PostgreSQL config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -136,13 +141,15 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set PGPASSWORD env: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = os.Unsetenv("PGPASSWORD") }()
|
||||
|
||||
start, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_postmaster_start_time();" | head -1 | cut -d'.' -f1`)
|
||||
port := s.getPort()
|
||||
start, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_postmaster_start_time();" | head -1 | cut -d'.' -f1`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL start time: %v", err))
|
||||
return
|
||||
}
|
||||
pid, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_backend_pid();"`)
|
||||
pid, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_backend_pid();"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL backend pid: %v", err))
|
||||
return
|
||||
@@ -152,22 +159,17 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL process: %v", err))
|
||||
return
|
||||
}
|
||||
connections, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();"`)
|
||||
connections, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL connections: %v", err))
|
||||
return
|
||||
}
|
||||
storage, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_size_pretty(pg_database_size('postgres'));"`)
|
||||
storage, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_size_pretty(pg_database_size('postgres'));"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL database size: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Unsetenv("PGPASSWORD"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to unset PGPASSWORD env: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Start Time"), Value: start},
|
||||
{Name: s.t.Get("Process PID"), Value: pid},
|
||||
@@ -210,10 +212,11 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
oldPassword, _ := s.settingRepo.Get(biz.SettingKeyPostgresPassword)
|
||||
postgres, err := db.NewPostgres("postgres", oldPassword, "127.0.0.1", 5432)
|
||||
port := s.getPort()
|
||||
postgres, err := db.NewPostgres(r.Context(), "postgres", oldPassword, "127.0.0.1", port)
|
||||
if err != nil {
|
||||
// 直接修改密码
|
||||
if _, err = shell.Execf(`su - postgres -c "psql -c \"ALTER USER postgres WITH PASSWORD '%s';\""`, req.Password); err != nil {
|
||||
if _, err = shell.Execf(`su - postgres -c "psql -p %d -c \"ALTER USER postgres WITH PASSWORD '%s';\""`, port, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set postgres password: %v", err))
|
||||
return
|
||||
}
|
||||
@@ -237,7 +240,7 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfigTune 获取 PostgreSQL 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root))
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -284,7 +287,8 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root)
|
||||
confPath := s.configPath()
|
||||
oldPort := s.getPort()
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -324,14 +328,48 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("postgresql"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload PostgreSQL: %v", err))
|
||||
if err = s.applyConfig(config, oldPort); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to apply PostgreSQL config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root)
|
||||
}
|
||||
|
||||
// getPort 读取 PostgreSQL 端口
|
||||
func (s *App) getPort() uint {
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
return 5432
|
||||
}
|
||||
return s.parsePort(config)
|
||||
}
|
||||
|
||||
// parsePort 从 config 内容解析端口,未配置时返回默认值
|
||||
func (s *App) parsePort(config string) uint {
|
||||
port := cast.ToUint(s.getPGValue(config, "port"))
|
||||
if port == 0 {
|
||||
return 5432
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// applyConfig 让 PostgreSQL 配置生效
|
||||
func (s *App) applyConfig(newConfig string, oldPort uint) error {
|
||||
newPort := s.parsePort(newConfig)
|
||||
if oldPort == newPort {
|
||||
return systemctl.Reload("postgresql")
|
||||
}
|
||||
if err := systemctl.Restart("postgresql"); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.databaseServerRepo.UpdatePort("local_postgresql", newPort)
|
||||
}
|
||||
|
||||
// getPGValue 从 PostgreSQL 配置内容中获取指定键的值
|
||||
func (s *App) getPGValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
|
||||
@@ -5,14 +5,14 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type SetPostgresPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// ConfigTune PostgreSQL 配置调整
|
||||
type ConfigTune struct {
|
||||
// 连接设置
|
||||
ListenAddresses string `form:"listen_addresses" json:"listen_addresses"`
|
||||
Port string `form:"port" json:"port"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
SuperuserReservedConnections string `form:"superuser_reserved_connections" json:"superuser_reserved_connections"`
|
||||
// 内存设置
|
||||
|
||||
@@ -29,8 +29,9 @@ type App struct {
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, conf *config.Config, taskRepo biz.TaskRepo) *App {
|
||||
return &App{t: t, conf: conf, taskRepo: taskRepo}
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, taskRepo biz.TaskRepo) (*App, error) {
|
||||
|
||||
return &App{t: t, conf: conf, taskRepo: taskRepo}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -73,7 +74,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get("http://127.0.0.1:9090/api/v1/status/runtimeinfo")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -259,6 +260,7 @@ func (s *App) InstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/prometheus/exporters/exporter.sh' | bash -s -- 'install' '%s'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "prometheus:exporter:" + req.Slug
|
||||
task.Name = s.t.Get("Install Prometheus exporter %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
@@ -286,6 +288,7 @@ func (s *App) UninstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/prometheus/exporters/exporter.sh' | bash -s -- 'uninstall' '%s'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "prometheus:exporter:" + req.Slug
|
||||
task.Name = s.t.Get("Uninstall Prometheus exporter %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
@@ -25,10 +25,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -2,7 +2,7 @@ package pureftpd
|
||||
|
||||
type Create struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -12,21 +12,21 @@ type Delete struct {
|
||||
|
||||
type ChangePassword struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"`
|
||||
Port uint `form:"port" json:"port" validate:"required && number && min:1 && max:65535"`
|
||||
}
|
||||
|
||||
// ConfigTune Pure-FTPd 配置调整
|
||||
type ConfigTune struct {
|
||||
MaxClientsNumber string `form:"max_clients_number" json:"max_clients_number"`
|
||||
MaxClientsPerIP string `form:"max_clients_per_ip" json:"max_clients_per_ip"`
|
||||
MaxIdleTime string `form:"max_idle_time" json:"max_idle_time"`
|
||||
MaxClientsNumber string `form:"max_clients_number" json:"max_clients_number" validate:"number && min:1"`
|
||||
MaxClientsPerIP string `form:"max_clients_per_ip" json:"max_clients_per_ip" validate:"number && min:1"`
|
||||
MaxIdleTime string `form:"max_idle_time" json:"max_idle_time" validate:"number && min:0"`
|
||||
MaxLoad string `form:"max_load" json:"max_load"`
|
||||
PassivePortRange string `form:"passive_port_range" json:"passive_port_range"`
|
||||
AnonymousOnly string `form:"anonymous_only" json:"anonymous_only"`
|
||||
NoAnonymous string `form:"no_anonymous" json:"no_anonymous"`
|
||||
MaxDiskUsage string `form:"max_disk_usage" json:"max_disk_usage"`
|
||||
AnonymousOnly string `form:"anonymous_only" json:"anonymous_only" validate:"in:yes,no"`
|
||||
NoAnonymous string `form:"no_anonymous" json:"no_anonymous" validate:"in:yes,no"`
|
||||
MaxDiskUsage string `form:"max_disk_usage" json:"max_disk_usage" validate:"number && min:1 && max:100"`
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) (*App, error) {
|
||||
|
||||
databaseServer := databaseServerRepo
|
||||
return &App{
|
||||
t: t,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -8,15 +8,15 @@ type UpdateConfig struct {
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Bind string `form:"bind" json:"bind"`
|
||||
Port string `form:"port" json:"port"`
|
||||
Databases string `form:"databases" json:"databases"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
Databases string `form:"databases" json:"databases" validate:"number && min:1"`
|
||||
Requirepass string `form:"requirepass" json:"requirepass"`
|
||||
Timeout string `form:"timeout" json:"timeout"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive"`
|
||||
Timeout string `form:"timeout" json:"timeout" validate:"number"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive" validate:"number"`
|
||||
// 内存
|
||||
Maxmemory string `form:"maxmemory" json:"maxmemory"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy" validate:"in:noeviction,allkeys-lru,allkeys-lfu,allkeys-random,volatile-lru,volatile-lfu,volatile-random,volatile-ttl"`
|
||||
// 持久化
|
||||
Appendonly string `form:"appendonly" json:"appendonly"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync"`
|
||||
Appendonly string `form:"appendonly" json:"appendonly" validate:"in:yes,no"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync" validate:"in:always,everysec,no"`
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
|
||||
return &App{t: t}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -8,15 +8,15 @@ type UpdateConfig struct {
|
||||
// ConfigTune RocketMQ 配置调整
|
||||
type ConfigTune struct {
|
||||
// Broker 基础
|
||||
BrokerName string `form:"broker_name" json:"broker_name"` // brokerName
|
||||
ListenPort string `form:"listen_port" json:"listen_port"` // listenPort
|
||||
NamesrvAddr string `form:"namesrv_addr" json:"namesrv_addr"` // namesrvAddr
|
||||
BrokerRole string `form:"broker_role" json:"broker_role"` // brokerRole
|
||||
FlushDiskType string `form:"flush_disk_type" json:"flush_disk_type"` // flushDiskType
|
||||
BrokerName string `form:"broker_name" json:"broker_name"` // brokerName
|
||||
ListenPort string `form:"listen_port" json:"listen_port" validate:"number && min:1 && max:65535"` // listenPort
|
||||
NamesrvAddr string `form:"namesrv_addr" json:"namesrv_addr"` // namesrvAddr
|
||||
BrokerRole string `form:"broker_role" json:"broker_role" validate:"in:ASYNC_MASTER,SYNC_MASTER,SLAVE"` // brokerRole
|
||||
FlushDiskType string `form:"flush_disk_type" json:"flush_disk_type" validate:"in:ASYNC_FLUSH,SYNC_FLUSH"` // flushDiskType
|
||||
// 存储
|
||||
StorePathRootDir string `form:"store_path_root_dir" json:"store_path_root_dir"` // storePathRootDir
|
||||
StorePathCommitLog string `form:"store_path_commit_log" json:"store_path_commit_log"` // storePathCommitLog
|
||||
MaxMessageSize string `form:"max_message_size" json:"max_message_size"` // maxMessageSize
|
||||
StorePathRootDir string `form:"store_path_root_dir" json:"store_path_root_dir" validate:"unix_path"` // storePathRootDir
|
||||
StorePathCommitLog string `form:"store_path_commit_log" json:"store_path_commit_log" validate:"unix_path"` // storePathCommitLog
|
||||
MaxMessageSize string `form:"max_message_size" json:"max_message_size" validate:"number"` // maxMessageSize
|
||||
// JVM - NameServer
|
||||
NamesrvHeapInitSize string `form:"namesrv_heap_init_size" json:"namesrv_heap_init_size"` // -Xms (namesrv)
|
||||
NamesrvHeapMaxSize string `form:"namesrv_heap_max_size" json:"namesrv_heap_max_size"` // -Xmx (namesrv)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/libtnb/utils/str"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -22,10 +22,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
@@ -23,10 +23,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -4,8 +4,8 @@ type Create struct {
|
||||
Ak string `form:"ak" json:"ak" validate:"required"`
|
||||
Sk string `form:"sk" json:"sk" validate:"required"`
|
||||
Bucket string `form:"bucket" json:"bucket" validate:"required"`
|
||||
URL string `form:"url" json:"url" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
URL string `form:"url" json:"url" validate:"required && url"`
|
||||
Path string `form:"path" json:"path" validate:"required && unix_path"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -22,7 +22,8 @@ type App struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
|
||||
var name string
|
||||
if os.IsRHEL() {
|
||||
name = "supervisord"
|
||||
@@ -33,7 +34,7 @@ func NewApp(t *gotext.Locale) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
name: name,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -16,7 +16,7 @@ type ProcessName struct {
|
||||
type CreateProcess struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
User string `form:"user" json:"user" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required && unix_path"`
|
||||
Command string `form:"command" json:"command" validate:"required"`
|
||||
Num int `form:"num" json:"num" validate:"required|min:1"`
|
||||
Num int `form:"num" json:"num" validate:"required && min:1"`
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) (*App, error) {
|
||||
|
||||
databaseServer := databaseServerRepo
|
||||
return &App{
|
||||
t: t,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -8,15 +8,15 @@ type UpdateConfig struct {
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Bind string `form:"bind" json:"bind"`
|
||||
Port string `form:"port" json:"port"`
|
||||
Databases string `form:"databases" json:"databases"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
Databases string `form:"databases" json:"databases" validate:"number && min:1"`
|
||||
Requirepass string `form:"requirepass" json:"requirepass"`
|
||||
Timeout string `form:"timeout" json:"timeout"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive"`
|
||||
Timeout string `form:"timeout" json:"timeout" validate:"number"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive" validate:"number"`
|
||||
// 内存
|
||||
Maxmemory string `form:"maxmemory" json:"maxmemory"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy" validate:"in:noeviction,allkeys-lru,allkeys-lfu,volatile-lru,volatile-lfu,allkeys-random,volatile-random,volatile-ttl"`
|
||||
// 持久化
|
||||
Appendonly string `form:"appendonly" json:"appendonly"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync"`
|
||||
Appendonly string `form:"appendonly" json:"appendonly" validate:"in:yes,no"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync" validate:"in:always,everysec,no"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,984 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
lop "github.com/samber/lo/parallel"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/apploader"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/sshlog"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
// 告警指标类型
|
||||
const (
|
||||
AlertTypeCPU = "cpu" // CPU 使用率 %
|
||||
AlertTypeMemory = "memory" // 内存使用率 %
|
||||
AlertTypeSwap = "swap" // Swap 使用率 %
|
||||
AlertTypeLoad1 = "load1" // 1 分钟平均负载
|
||||
AlertTypeLoad5 = "load5" // 5 分钟平均负载
|
||||
AlertTypeLoad15 = "load15" // 15 分钟平均负载
|
||||
AlertTypeDisk = "disk" // 磁盘使用率 %,目标为挂载点
|
||||
AlertTypeDiskInode = "disk_inode" // 磁盘 inode 使用率 %,目标为挂载点
|
||||
AlertTypeDiskRead = "disk_read" // 磁盘读取速率 MB/s,目标为设备名
|
||||
AlertTypeDiskWrite = "disk_write" // 磁盘写入速率 MB/s,目标为设备名
|
||||
AlertTypeNetIn = "net_in" // 网卡下行速率 MB/s,目标为网卡名
|
||||
AlertTypeNetOut = "net_out" // 网卡上行速率 MB/s,目标为网卡名
|
||||
AlertTypeWebsite5xx = "website_5xx" // 网站本小时 5xx 次数,目标为网站名
|
||||
AlertTypeWebsiteError = "website_error" // 网站本小时错误率 %,目标为网站名
|
||||
AlertTypeService = "service" // 服务未运行,目标为服务名
|
||||
AlertTypeProject = "project" // 项目未运行,目标为项目名
|
||||
AlertTypeContainer = "container" // 容器未运行,目标为容器名
|
||||
AlertTypeApp = "app" // 应用未运行,目标为应用标识
|
||||
AlertTypeDatabase = "database" // 数据库服务器不可达,目标为服务器名
|
||||
AlertTypeCertExpire = "cert_expire" // 证书剩余天数,目标为域名
|
||||
AlertTypeWebsiteExpire = "website_expire" // 网站剩余天数,目标为网站名
|
||||
)
|
||||
|
||||
// statusAlertTypes 状态类指标,语义固定为「不在运行」,不需要运算符与阈值
|
||||
var statusAlertTypes = []string{AlertTypeService, AlertTypeProject, AlertTypeContainer, AlertTypeApp, AlertTypeDatabase}
|
||||
|
||||
// IsStatusAlert 是否为状态类指标
|
||||
func IsStatusAlert(typ string) bool {
|
||||
return slices.Contains(statusAlertTypes, typ)
|
||||
}
|
||||
|
||||
const (
|
||||
// alertRetryDelay 通知发送失败后的重试间隔
|
||||
alertRetryDelay = 5 * time.Minute
|
||||
// sshFailThreshold 单次检查窗口内触发爆破告警的失败次数
|
||||
sshFailThreshold uint = 5
|
||||
// sshFailSilence 同一来源两次爆破告警的最小间隔
|
||||
sshFailSilence = 30 * time.Minute
|
||||
)
|
||||
|
||||
// 告警比较运算符
|
||||
const (
|
||||
AlertOperatorGT = "gt"
|
||||
AlertOperatorGTE = "gte"
|
||||
AlertOperatorLT = "lt"
|
||||
AlertOperatorLTE = "lte"
|
||||
)
|
||||
|
||||
// AlertRule 告警规则
|
||||
type AlertRule struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;default:''" json:"name"`
|
||||
Type string `gorm:"not null;default:''" json:"type"`
|
||||
Target string `gorm:"not null;default:''" json:"target"` // 挂载点/网卡/磁盘/服务名/域名,空表示全部
|
||||
Operator string `gorm:"not null;default:'gt'" json:"operator"` // gt/gte/lt/lte
|
||||
Threshold float64 `gorm:"not null;default:0" json:"threshold"`
|
||||
Duration uint `gorm:"not null;default:1" json:"duration"` // 连续满足次数
|
||||
Silence uint `gorm:"not null;default:30" json:"silence"` // 静默期(分钟)
|
||||
Channels []uint `gorm:"serializer:json" json:"channels"` // 通知渠道 ID
|
||||
Enabled bool `gorm:"not null;default:true" json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Alert 告警记录
|
||||
type Alert struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
RuleID uint `gorm:"not null;default:0;index" json:"rule_id"`
|
||||
RuleName string `gorm:"not null;default:''" json:"rule_name"`
|
||||
Type string `gorm:"not null;default:''" json:"type"`
|
||||
Target string `gorm:"not null;default:''" json:"target"`
|
||||
Value float64 `gorm:"not null;default:0" json:"value"`
|
||||
Message string `gorm:"not null;default:''" json:"message"`
|
||||
Notified bool `gorm:"not null;default:false" json:"notified"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
}
|
||||
|
||||
// AlertMetric 单个目标的取值
|
||||
type AlertMetric struct {
|
||||
Target string
|
||||
Value float64
|
||||
}
|
||||
|
||||
type AlertRepo interface {
|
||||
ListRules(page, limit uint) ([]*AlertRule, int64, error)
|
||||
AllRules() ([]*AlertRule, error)
|
||||
GetRule(id uint) (*AlertRule, error)
|
||||
CreateRule(rule *AlertRule) error
|
||||
UpdateRule(rule *AlertRule) error
|
||||
DeleteRule(id uint) error
|
||||
AddAlert(alert *Alert) error
|
||||
ListAlerts(page, limit uint) ([]*Alert, int64, error)
|
||||
ClearAlerts() error
|
||||
ClearAlertsBefore(t time.Time) error
|
||||
CertExpiry() ([]*AlertMetric, error)
|
||||
WebsiteExpiry() ([]*AlertMetric, error)
|
||||
ProjectNames() ([]string, error)
|
||||
DatabaseServers() ([]*DatabaseServer, error)
|
||||
WebsiteHourStats() ([]*WebsiteHourStat, error)
|
||||
}
|
||||
|
||||
// WebsiteHourStat 网站当前小时的请求统计
|
||||
type WebsiteHourStat struct {
|
||||
Site string
|
||||
Requests uint64
|
||||
Errors uint64
|
||||
Status5xx uint64
|
||||
}
|
||||
|
||||
// ioSnapshot 网卡/磁盘累计计数快照及据此算出的速率(字节/秒)
|
||||
type ioSnapshot struct {
|
||||
in uint64
|
||||
out uint64
|
||||
inRate uint64
|
||||
outRate uint64
|
||||
at time.Time
|
||||
}
|
||||
|
||||
type AlertUsecase struct {
|
||||
repo AlertRepo
|
||||
notify *NotifyUsecase
|
||||
setting SettingRepo
|
||||
container ContainerRepo
|
||||
app AppRepo
|
||||
database DatabaseServerRepo
|
||||
loader *apploader.Loader
|
||||
log *slog.Logger
|
||||
t *gotext.Locale
|
||||
|
||||
mu sync.Mutex
|
||||
hits map[string]uint // 连续命中次数
|
||||
fired map[string]time.Time // 上次通知时间
|
||||
netSnaps map[string]ioSnapshot // 网卡累计流量快照
|
||||
diskSnaps map[string]ioSnapshot // 磁盘累计 IO 快照
|
||||
healthKeys map[string]struct{} // 已通知的健康问题
|
||||
sshFired map[string]time.Time // SSH 爆破上次通知时间
|
||||
sshAt time.Time // SSH 日志上次检查时间
|
||||
dbProbe map[string]bool // 本轮数据库连通性探测结果
|
||||
cleanedAt time.Time
|
||||
}
|
||||
|
||||
func NewAlertUsecase(notifyUsecase *NotifyUsecase, loader *apploader.Loader, t *gotext.Locale, log *slog.Logger, alertRepo AlertRepo, appRepo AppRepo, containerRepo ContainerRepo, databaseServerRepo DatabaseServerRepo, settingRepo SettingRepo) (*AlertUsecase, error) {
|
||||
return &AlertUsecase{
|
||||
repo: alertRepo,
|
||||
notify: notifyUsecase,
|
||||
setting: settingRepo,
|
||||
container: containerRepo,
|
||||
app: appRepo,
|
||||
database: databaseServerRepo,
|
||||
loader: loader,
|
||||
log: log,
|
||||
t: t,
|
||||
hits: make(map[string]uint),
|
||||
fired: make(map[string]time.Time),
|
||||
netSnaps: make(map[string]ioSnapshot),
|
||||
diskSnaps: make(map[string]ioSnapshot),
|
||||
healthKeys: make(map[string]struct{}),
|
||||
sshFired: make(map[string]time.Time),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) ListRules(page, limit uint) ([]*AlertRule, int64, error) {
|
||||
return uc.repo.ListRules(page, limit)
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) GetRule(id uint) (*AlertRule, error) {
|
||||
return uc.repo.GetRule(id)
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) CreateRule(ctx context.Context, req *request.AlertRuleCreate) (*AlertRule, error) {
|
||||
rule := &AlertRule{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Target: req.Target,
|
||||
Operator: req.Operator,
|
||||
Threshold: req.Threshold,
|
||||
Duration: req.Duration,
|
||||
Silence: req.Silence,
|
||||
Channels: req.Channels,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
normalizeRule(rule)
|
||||
|
||||
if err := uc.repo.CreateRule(rule); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.log.Info("alert rule created", slog.String("type", OperationTypeMonitor), slog.Uint64("operator_id", operatorID(ctx)), slog.String("name", req.Name))
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) UpdateRule(ctx context.Context, req *request.AlertRuleUpdate) error {
|
||||
rule, err := uc.repo.GetRule(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rule.Name = req.Name
|
||||
rule.Type = req.Type
|
||||
rule.Target = req.Target
|
||||
rule.Operator = req.Operator
|
||||
rule.Threshold = req.Threshold
|
||||
rule.Duration = req.Duration
|
||||
rule.Silence = req.Silence
|
||||
rule.Channels = req.Channels
|
||||
rule.Enabled = req.Enabled
|
||||
normalizeRule(rule)
|
||||
|
||||
if err = uc.repo.UpdateRule(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 规则变更后重置命中状态,避免沿用旧阈值的计数
|
||||
uc.mu.Lock()
|
||||
uc.clearState(rule.ID)
|
||||
uc.mu.Unlock()
|
||||
|
||||
uc.log.Info("alert rule updated", slog.String("type", OperationTypeMonitor), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", req.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) DeleteRule(ctx context.Context, id uint) error {
|
||||
rule, err := uc.repo.GetRule(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = uc.repo.DeleteRule(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.mu.Lock()
|
||||
uc.clearState(id)
|
||||
uc.mu.Unlock()
|
||||
|
||||
uc.log.Info("alert rule deleted", slog.String("type", OperationTypeMonitor), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", rule.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) ListAlerts(page, limit uint) ([]*Alert, int64, error) {
|
||||
return uc.repo.ListAlerts(page, limit)
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) ClearAlerts() error {
|
||||
return uc.repo.ClearAlerts()
|
||||
}
|
||||
|
||||
// Evaluate 评估全部启用的规则,命中则记录并通知
|
||||
func (uc *AlertUsecase) Evaluate(ctx context.Context) error {
|
||||
rules, err := uc.repo.AllRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 探测结果按轮缓存,进入新一轮先失效
|
||||
uc.mu.Lock()
|
||||
uc.dbProbe = nil
|
||||
uc.mu.Unlock()
|
||||
|
||||
uc.cleanup()
|
||||
uc.checkHealth(ctx)
|
||||
uc.checkSSH(ctx)
|
||||
|
||||
var enabled []*AlertRule
|
||||
for _, rule := range rules {
|
||||
if rule.Enabled {
|
||||
enabled = append(enabled, rule)
|
||||
}
|
||||
}
|
||||
if len(enabled) == 0 {
|
||||
uc.mu.Lock()
|
||||
clear(uc.hits)
|
||||
clear(uc.fired)
|
||||
uc.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
info := tools.CurrentInfo(nil, nil)
|
||||
now := time.Now()
|
||||
uc.updateSnapshots(info, now)
|
||||
|
||||
alive := make(map[string]struct{})
|
||||
for _, rule := range enabled {
|
||||
metrics, err := uc.collect(ctx, rule, info)
|
||||
if err != nil {
|
||||
uc.log.Warn("failed to collect alert metric", slog.String("rule", rule.Name), slog.Any("err", err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, metric := range metrics {
|
||||
key := stateKey(rule.ID, metric.Target)
|
||||
alive[key] = struct{}{}
|
||||
uc.evaluateMetric(ctx, rule, metric, key, now)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已消失的目标状态
|
||||
uc.mu.Lock()
|
||||
for key := range uc.hits {
|
||||
if _, ok := alive[key]; !ok {
|
||||
delete(uc.hits, key)
|
||||
}
|
||||
}
|
||||
uc.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) evaluateMetric(ctx context.Context, rule *AlertRule, metric *AlertMetric, key string, now time.Time) {
|
||||
uc.mu.Lock()
|
||||
if !matchThreshold(rule, metric.Value) {
|
||||
delete(uc.hits, key)
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
uc.hits[key]++
|
||||
hits := uc.hits[key]
|
||||
duration := max(rule.Duration, 1)
|
||||
if hits < duration {
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 静默期内不重复告警
|
||||
if last, ok := uc.fired[key]; ok && now.Sub(last) < time.Duration(rule.Silence)*time.Minute {
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
uc.fired[key] = now
|
||||
uc.mu.Unlock()
|
||||
|
||||
alert := &Alert{
|
||||
RuleID: rule.ID,
|
||||
RuleName: rule.Name,
|
||||
Type: rule.Type,
|
||||
Target: metric.Target,
|
||||
Value: metric.Value,
|
||||
Message: uc.buildMessage(rule, metric),
|
||||
}
|
||||
|
||||
sent, err := uc.notify.Send(ctx, rule.Channels, uc.t.Get("[AcePanel] Alert: %s", rule.Name), NotifyBody(alert.Message, [][2]string{
|
||||
{uc.t.Get("Rule"), rule.Name},
|
||||
{uc.t.Get("Metric"), uc.metricLabel(rule.Type, metric.Target)},
|
||||
{uc.t.Get("Current Value"), uc.formatValue(rule.Type, metric.Value)},
|
||||
{uc.t.Get("Threshold"), uc.formatValue(rule.Type, rule.Threshold)},
|
||||
{uc.t.Get("Time"), now.Format(time.DateTime)},
|
||||
}))
|
||||
if err != nil {
|
||||
uc.log.Warn("failed to send alert notification", slog.String("rule", rule.Name), slog.Any("err", err))
|
||||
}
|
||||
alert.Notified = sent > 0
|
||||
|
||||
// 一条都没送达时缩短静默期,让下一轮重试,避免临时故障吞掉整个静默窗口
|
||||
if len(rule.Channels) > 0 && sent == 0 {
|
||||
uc.mu.Lock()
|
||||
uc.fired[key] = now.Add(alertRetryDelay - time.Duration(rule.Silence)*time.Minute)
|
||||
uc.mu.Unlock()
|
||||
}
|
||||
|
||||
if err := uc.repo.AddAlert(alert); err != nil {
|
||||
uc.log.Warn("failed to save alert record", slog.String("rule", rule.Name), slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
|
||||
// collect 采集规则对应的目标取值
|
||||
func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types.CurrentInfo) ([]*AlertMetric, error) {
|
||||
switch rule.Type {
|
||||
case AlertTypeCPU:
|
||||
return []*AlertMetric{{Value: info.Percent}}, nil
|
||||
|
||||
case AlertTypeMemory:
|
||||
if info.Mem == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []*AlertMetric{{Value: info.Mem.UsedPercent}}, nil
|
||||
|
||||
case AlertTypeSwap:
|
||||
if info.Swap == nil || info.Swap.Total == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return []*AlertMetric{{Value: info.Swap.UsedPercent}}, nil
|
||||
|
||||
case AlertTypeLoad1, AlertTypeLoad5, AlertTypeLoad15:
|
||||
if info.Load == nil {
|
||||
return nil, nil
|
||||
}
|
||||
switch rule.Type {
|
||||
case AlertTypeLoad1:
|
||||
return []*AlertMetric{{Value: info.Load.Load1}}, nil
|
||||
case AlertTypeLoad5:
|
||||
return []*AlertMetric{{Value: info.Load.Load5}}, nil
|
||||
default:
|
||||
return []*AlertMetric{{Value: info.Load.Load15}}, nil
|
||||
}
|
||||
|
||||
case AlertTypeDisk, AlertTypeDiskInode:
|
||||
metrics := make([]*AlertMetric, 0)
|
||||
for _, usage := range info.DiskUsage {
|
||||
if rule.Target != "" && rule.Target != usage.Path {
|
||||
continue
|
||||
}
|
||||
value := usage.UsedPercent
|
||||
if rule.Type == AlertTypeDiskInode {
|
||||
value = usage.InodesUsedPercent
|
||||
}
|
||||
metrics = append(metrics, &AlertMetric{Target: usage.Path, Value: value})
|
||||
}
|
||||
return metrics, nil
|
||||
|
||||
case AlertTypeNetIn, AlertTypeNetOut, AlertTypeDiskRead, AlertTypeDiskWrite:
|
||||
return uc.rateMetrics(rule), nil
|
||||
|
||||
case AlertTypeWebsite5xx, AlertTypeWebsiteError:
|
||||
stats, err := uc.repo.WebsiteHourStats()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics := make([]*AlertMetric, 0, len(stats))
|
||||
for _, item := range stats {
|
||||
if rule.Target != "" && rule.Target != item.Site {
|
||||
continue
|
||||
}
|
||||
if rule.Type == AlertTypeWebsite5xx {
|
||||
metrics = append(metrics, &AlertMetric{Target: item.Site, Value: float64(item.Status5xx)})
|
||||
continue
|
||||
}
|
||||
// 无请求时错误率无意义
|
||||
if item.Requests == 0 {
|
||||
continue
|
||||
}
|
||||
metrics = append(metrics, &AlertMetric{Target: item.Site, Value: float64(item.Errors) / float64(item.Requests) * 100})
|
||||
}
|
||||
return metrics, nil
|
||||
|
||||
case AlertTypeService:
|
||||
if rule.Target == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return []*AlertMetric{{Target: rule.Target, Value: notRunning(systemctl.Status(rule.Target))}}, nil
|
||||
|
||||
case AlertTypeProject:
|
||||
names, err := uc.repo.ProjectNames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 项目即 systemd 单元,单元名与项目名一致
|
||||
return lop.Map(filterNames(rule.Target, names), func(name string, _ int) *AlertMetric {
|
||||
return &AlertMetric{Target: name, Value: notRunning(systemctl.Status(name))}
|
||||
}), nil
|
||||
|
||||
case AlertTypeContainer:
|
||||
containers, err := uc.container.ListAll(containerSock(uc.setting))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics := make([]*AlertMetric, 0, len(containers))
|
||||
for _, item := range containers {
|
||||
if rule.Target != "" && rule.Target != item.Name {
|
||||
continue
|
||||
}
|
||||
value := float64(0)
|
||||
if item.State != "running" {
|
||||
value = 1
|
||||
}
|
||||
metrics = append(metrics, &AlertMetric{Target: item.Name, Value: value})
|
||||
}
|
||||
return metrics, nil
|
||||
|
||||
case AlertTypeApp:
|
||||
installed, err := uc.app.Installed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets := lo.Filter(installed, func(item *App, _ int) bool {
|
||||
if rule.Target != "" && rule.Target != item.Slug {
|
||||
return false
|
||||
}
|
||||
_, ok := uc.loader.Get(item.Slug)
|
||||
return ok
|
||||
})
|
||||
// 状态查询逐个访问 systemd,并发采集
|
||||
return lo.Compact(lop.Map(targets, func(item *App, _ int) *AlertMetric {
|
||||
a, _ := uc.loader.Get(item.Slug)
|
||||
status := a.Status()
|
||||
// 无 systemd 服务的应用没有运行状态
|
||||
if status == types.AppStatusNA {
|
||||
return nil
|
||||
}
|
||||
value := float64(0)
|
||||
if status != types.AppStatusRunning {
|
||||
value = 1
|
||||
}
|
||||
return &AlertMetric{Target: item.Slug, Value: value}
|
||||
})), nil
|
||||
|
||||
case AlertTypeDatabase:
|
||||
servers, err := uc.repo.DatabaseServers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reachable := uc.probeDatabases(ctx, servers)
|
||||
|
||||
metrics := make([]*AlertMetric, 0, len(servers))
|
||||
for _, item := range servers {
|
||||
if rule.Target != "" && rule.Target != item.Name {
|
||||
continue
|
||||
}
|
||||
value := float64(0)
|
||||
if !reachable[item.Name] {
|
||||
value = 1
|
||||
}
|
||||
metrics = append(metrics, &AlertMetric{Target: item.Name, Value: value})
|
||||
}
|
||||
return metrics, nil
|
||||
|
||||
case AlertTypeCertExpire:
|
||||
metrics, err := uc.repo.CertExpiry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filterTarget(rule.Target, metrics), nil
|
||||
|
||||
case AlertTypeWebsiteExpire:
|
||||
metrics, err := uc.repo.WebsiteExpiry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filterTarget(rule.Target, metrics), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported alert type: %s", rule.Type)
|
||||
}
|
||||
|
||||
// notRunning 将运行状态转为告警取值,未运行记为 1,配合 normalizeRule 的 >=1 判定
|
||||
func notRunning(running bool, _ error) float64 {
|
||||
if running {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// filterNames 按目标名筛选,目标为空表示全部
|
||||
func filterNames(target string, names []string) []string {
|
||||
if target == "" {
|
||||
return names
|
||||
}
|
||||
if slices.Contains(names, target) {
|
||||
return []string{target}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterTarget 按目标名筛选,目标为空表示全部
|
||||
// 证书目标是逗号分隔的多域名,允许用其中任一域名命中
|
||||
func filterTarget(target string, metrics []*AlertMetric) []*AlertMetric {
|
||||
if target == "" {
|
||||
return metrics
|
||||
}
|
||||
|
||||
filtered := make([]*AlertMetric, 0)
|
||||
for _, metric := range metrics {
|
||||
if metric.Target == target || slices.Contains(strings.Split(metric.Target, ","), target) {
|
||||
filtered = append(filtered, metric)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// probeDatabases 并发探测数据库连通性,结果在本轮评估内复用,避免多条规则重复整批探测
|
||||
// 单台耗时上限由 pkg/db 各驱动的连接超时保证(5~10 秒),并发后整批不会拖长分钟级评估
|
||||
func (uc *AlertUsecase) probeDatabases(ctx context.Context, servers []*DatabaseServer) map[string]bool {
|
||||
uc.mu.Lock()
|
||||
cached := uc.dbProbe
|
||||
uc.mu.Unlock()
|
||||
if cached != nil {
|
||||
return cached
|
||||
}
|
||||
|
||||
probe := lo.FromEntries(lop.Map(servers, func(item *DatabaseServer, _ int) lo.Entry[string, bool] {
|
||||
return lo.Entry[string, bool]{Key: item.Name, Value: uc.database.CheckServer(ctx, item)}
|
||||
}))
|
||||
|
||||
uc.mu.Lock()
|
||||
uc.dbProbe = probe
|
||||
uc.mu.Unlock()
|
||||
|
||||
return probe
|
||||
}
|
||||
|
||||
// rateMetrics 从快照读取速率指标(MB/s)
|
||||
func (uc *AlertUsecase) rateMetrics(rule *AlertRule) []*AlertMetric {
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
|
||||
snaps := uc.netSnaps
|
||||
if rule.Type == AlertTypeDiskRead || rule.Type == AlertTypeDiskWrite {
|
||||
snaps = uc.diskSnaps
|
||||
}
|
||||
|
||||
metrics := make([]*AlertMetric, 0, len(snaps))
|
||||
for name, snap := range snaps {
|
||||
if rule.Target != "" && rule.Target != name {
|
||||
continue
|
||||
}
|
||||
value := snap.inRate
|
||||
if rule.Type == AlertTypeNetOut || rule.Type == AlertTypeDiskWrite {
|
||||
value = snap.outRate
|
||||
}
|
||||
metrics = append(metrics, &AlertMetric{Target: name, Value: float64(value) / 1024 / 1024})
|
||||
}
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
// updateSnapshots 用两次采集的累计值计算速率,结果暂存于快照
|
||||
func (uc *AlertUsecase) updateSnapshots(info types.CurrentInfo, now time.Time) {
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
|
||||
nets := make(map[string]ioSnapshot, len(info.Net))
|
||||
for _, item := range info.Net {
|
||||
if item.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
nets[item.Name] = rateOf(uc.netSnaps[item.Name], item.BytesRecv, item.BytesSent, now)
|
||||
}
|
||||
uc.netSnaps = nets
|
||||
|
||||
disks := make(map[string]ioSnapshot, len(info.DiskIO))
|
||||
for _, item := range info.DiskIO {
|
||||
disks[item.Name] = rateOf(uc.diskSnaps[item.Name], item.ReadBytes, item.WriteBytes, now)
|
||||
}
|
||||
uc.diskSnaps = disks
|
||||
}
|
||||
|
||||
// checkHealth 上报新出现的面板健康问题,问题恢复后重新出现会再次通知
|
||||
// 同步发送并只在送达后记入去重集合,否则一次发送失败就会让持续存在的问题再也不告警
|
||||
func (uc *AlertUsecase) checkHealth(ctx context.Context) {
|
||||
issues := app.Health.Snapshot()
|
||||
|
||||
uc.mu.Lock()
|
||||
fresh := make([]app.HealthIssue, 0, len(issues))
|
||||
notified := make(map[string]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
if _, ok := uc.healthKeys[issue.Key]; ok {
|
||||
notified[issue.Key] = struct{}{}
|
||||
continue
|
||||
}
|
||||
fresh = append(fresh, issue)
|
||||
}
|
||||
// 已消失的问题在此被丢弃,恢复后再次出现会重新通知
|
||||
uc.healthKeys = notified
|
||||
uc.mu.Unlock()
|
||||
|
||||
for _, issue := range fresh {
|
||||
if err := uc.notify.SendEventSync(ctx, NotifyEventHealth, uc.t.Get("[AcePanel] Panel Health Issue"), NotifyBody(uc.t.Get("panel reported a health issue"), [][2]string{
|
||||
{uc.t.Get("Item"), issue.Key},
|
||||
{uc.t.Get("Level"), issue.Level},
|
||||
{uc.t.Get("Detail"), issue.Message},
|
||||
{uc.t.Get("Time"), issue.Since.Format(time.DateTime)},
|
||||
})); err != nil {
|
||||
uc.log.Warn("failed to send health notification", slog.String("item", issue.Key), slog.Any("err", err))
|
||||
continue
|
||||
}
|
||||
|
||||
uc.mu.Lock()
|
||||
uc.healthKeys[issue.Key] = struct{}{}
|
||||
uc.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// checkSSH 增量检查 sshd 日志,上报登录成功与爆破尝试
|
||||
func (uc *AlertUsecase) checkSSH(ctx context.Context) {
|
||||
now := time.Now()
|
||||
|
||||
uc.mu.Lock()
|
||||
since := uc.sshAt
|
||||
uc.sshAt = now
|
||||
uc.mu.Unlock()
|
||||
|
||||
// 首次仅记录时间,避免面板启动时把历史日志全推一遍
|
||||
if since.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := shell.ExecfWithContext(ctx, `journalctl -u sshd -u ssh --no-pager -o json --since "@%d" 2>/dev/null`, since.Unix())
|
||||
if err != nil {
|
||||
// 读取失败则回退检查点,下一轮重扫该窗口,避免丢掉这段时间的登录记录
|
||||
uc.mu.Lock()
|
||||
uc.sshAt = since
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if raw == "" {
|
||||
return
|
||||
}
|
||||
|
||||
failures := make(map[string]uint)
|
||||
scanner := bufio.NewScanner(strings.NewReader(raw))
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
var entry struct {
|
||||
Message string `json:"MESSAGE"`
|
||||
}
|
||||
if json.Unmarshal(scanner.Bytes(), &entry) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
record := sshlog.ParseMessage(entry.Message)
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch record.Status {
|
||||
case sshlog.StatusAccepted:
|
||||
uc.notify.SendEvent(NotifyEventSSHLogin, uc.t.Get("[AcePanel] SSH Login"), NotifyBody(uc.t.Get("SSH login succeeded"), [][2]string{
|
||||
{uc.t.Get("Username"), record.User},
|
||||
{uc.t.Get("IP"), record.IP},
|
||||
{uc.t.Get("Method"), record.Method},
|
||||
{uc.t.Get("Time"), now.Format(time.DateTime)},
|
||||
}))
|
||||
case sshlog.StatusFailed, sshlog.StatusInvalidUser:
|
||||
failures[record.IP]++
|
||||
}
|
||||
}
|
||||
|
||||
for ip, count := range failures {
|
||||
if count < sshFailThreshold || !uc.sshShouldFire(ip, now) {
|
||||
continue
|
||||
}
|
||||
uc.notify.SendEvent(NotifyEventSSHBruteforce, uc.t.Get("[AcePanel] SSH Brute-force Attempts"), NotifyBody(uc.t.Get("too many failed SSH login attempts"), [][2]string{
|
||||
{uc.t.Get("IP"), ip},
|
||||
{uc.t.Get("Failed Attempts"), cast.ToString(count)},
|
||||
{uc.t.Get("Time"), now.Format(time.DateTime)},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// sshShouldFire 判断某来源是否已过静默期,并顺带清理过期记录
|
||||
func (uc *AlertUsecase) sshShouldFire(ip string, now time.Time) bool {
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
|
||||
for key, at := range uc.sshFired {
|
||||
if now.Sub(at) > sshFailSilence {
|
||||
delete(uc.sshFired, key)
|
||||
}
|
||||
}
|
||||
|
||||
if at, ok := uc.sshFired[ip]; ok && now.Sub(at) < sshFailSilence {
|
||||
return false
|
||||
}
|
||||
uc.sshFired[ip] = now
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// cleanup 按保留天数清理历史告警记录
|
||||
func (uc *AlertUsecase) cleanup() {
|
||||
uc.mu.Lock()
|
||||
if time.Since(uc.cleanedAt) < 6*time.Hour {
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
uc.cleanedAt = time.Now()
|
||||
uc.mu.Unlock()
|
||||
|
||||
days, err := uc.setting.GetInt(SettingKeyAlertLogDays, 30)
|
||||
if err != nil || days <= 0 {
|
||||
return
|
||||
}
|
||||
if err = uc.repo.ClearAlertsBefore(time.Now().AddDate(0, 0, -days)); err != nil {
|
||||
uc.log.Warn("failed to clear expired alerts", slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
|
||||
// clearState 清除某条规则的运行时状态,调用前需持有锁
|
||||
func (uc *AlertUsecase) clearState(ruleID uint) {
|
||||
prefix := fmt.Sprintf("%d:", ruleID)
|
||||
for key := range uc.hits {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
delete(uc.hits, key)
|
||||
}
|
||||
}
|
||||
for key := range uc.fired {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
delete(uc.fired, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) buildMessage(rule *AlertRule, metric *AlertMetric) string {
|
||||
label := uc.metricLabel(rule.Type, metric.Target)
|
||||
|
||||
switch rule.Type {
|
||||
case AlertTypeService:
|
||||
return uc.t.Get("service %s is not running", metric.Target)
|
||||
case AlertTypeProject:
|
||||
return uc.t.Get("project %s is not running", metric.Target)
|
||||
case AlertTypeContainer:
|
||||
return uc.t.Get("container %s is not running", metric.Target)
|
||||
case AlertTypeApp:
|
||||
return uc.t.Get("app %s is not running", metric.Target)
|
||||
case AlertTypeDatabase:
|
||||
return uc.t.Get("database server %s is unreachable", metric.Target)
|
||||
case AlertTypeCertExpire:
|
||||
return uc.t.Get("certificate %s expires in %s days", metric.Target, uc.formatValue(rule.Type, metric.Value))
|
||||
case AlertTypeWebsiteExpire:
|
||||
return uc.t.Get("website %s expires in %s days", metric.Target, uc.formatValue(rule.Type, metric.Value))
|
||||
}
|
||||
|
||||
return uc.t.Get("%s is %s, %s threshold %s", label, uc.formatValue(rule.Type, metric.Value), uc.operatorLabel(rule.Operator), uc.formatValue(rule.Type, rule.Threshold))
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) metricLabel(typ, target string) string {
|
||||
var label string
|
||||
switch typ {
|
||||
case AlertTypeCPU:
|
||||
label = uc.t.Get("CPU usage")
|
||||
case AlertTypeMemory:
|
||||
label = uc.t.Get("memory usage")
|
||||
case AlertTypeSwap:
|
||||
label = uc.t.Get("swap usage")
|
||||
case AlertTypeLoad1:
|
||||
label = uc.t.Get("1 minute load")
|
||||
case AlertTypeLoad5:
|
||||
label = uc.t.Get("5 minutes load")
|
||||
case AlertTypeLoad15:
|
||||
label = uc.t.Get("15 minutes load")
|
||||
case AlertTypeDisk:
|
||||
label = uc.t.Get("disk usage")
|
||||
case AlertTypeDiskInode:
|
||||
label = uc.t.Get("disk inode usage")
|
||||
case AlertTypeDiskRead:
|
||||
label = uc.t.Get("disk read speed")
|
||||
case AlertTypeDiskWrite:
|
||||
label = uc.t.Get("disk write speed")
|
||||
case AlertTypeNetIn:
|
||||
label = uc.t.Get("network download speed")
|
||||
case AlertTypeNetOut:
|
||||
label = uc.t.Get("network upload speed")
|
||||
case AlertTypeWebsite5xx:
|
||||
label = uc.t.Get("website 5xx responses this hour")
|
||||
case AlertTypeWebsiteError:
|
||||
label = uc.t.Get("website error rate this hour")
|
||||
case AlertTypeService:
|
||||
label = uc.t.Get("service status")
|
||||
case AlertTypeProject:
|
||||
label = uc.t.Get("project status")
|
||||
case AlertTypeContainer:
|
||||
label = uc.t.Get("container status")
|
||||
case AlertTypeApp:
|
||||
label = uc.t.Get("app status")
|
||||
case AlertTypeDatabase:
|
||||
label = uc.t.Get("database server status")
|
||||
case AlertTypeCertExpire:
|
||||
label = uc.t.Get("certificate expiry")
|
||||
case AlertTypeWebsiteExpire:
|
||||
label = uc.t.Get("website expiry")
|
||||
default:
|
||||
label = typ
|
||||
}
|
||||
|
||||
if target != "" {
|
||||
return fmt.Sprintf("%s (%s)", label, target)
|
||||
}
|
||||
|
||||
return label
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) operatorLabel(operator string) string {
|
||||
switch operator {
|
||||
case AlertOperatorGTE:
|
||||
return uc.t.Get("greater than or equal to")
|
||||
case AlertOperatorLT:
|
||||
return uc.t.Get("less than")
|
||||
case AlertOperatorLTE:
|
||||
return uc.t.Get("less than or equal to")
|
||||
default:
|
||||
return uc.t.Get("greater than")
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) formatValue(typ string, value float64) string {
|
||||
switch typ {
|
||||
case AlertTypeCPU, AlertTypeMemory, AlertTypeSwap, AlertTypeDisk, AlertTypeDiskInode, AlertTypeWebsiteError:
|
||||
return fmt.Sprintf("%.2f%%", value)
|
||||
case AlertTypeNetIn, AlertTypeNetOut, AlertTypeDiskRead, AlertTypeDiskWrite:
|
||||
return fmt.Sprintf("%.2f MB/s", value)
|
||||
case AlertTypeCertExpire, AlertTypeWebsiteExpire, AlertTypeWebsite5xx:
|
||||
return fmt.Sprintf("%.0f", value)
|
||||
}
|
||||
|
||||
if IsStatusAlert(typ) {
|
||||
return uc.t.Get("not running")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f", value)
|
||||
}
|
||||
|
||||
// normalizeRule 补齐规则的默认值,状态类规则固定为「不在运行」
|
||||
func normalizeRule(rule *AlertRule) {
|
||||
if IsStatusAlert(rule.Type) {
|
||||
rule.Operator = AlertOperatorGTE
|
||||
rule.Threshold = 1
|
||||
}
|
||||
if rule.Duration < 1 {
|
||||
rule.Duration = 1
|
||||
}
|
||||
}
|
||||
|
||||
func matchThreshold(rule *AlertRule, value float64) bool {
|
||||
switch rule.Operator {
|
||||
case AlertOperatorGTE:
|
||||
return value >= rule.Threshold
|
||||
case AlertOperatorLT:
|
||||
return value < rule.Threshold
|
||||
case AlertOperatorLTE:
|
||||
return value <= rule.Threshold
|
||||
default:
|
||||
return value > rule.Threshold
|
||||
}
|
||||
}
|
||||
|
||||
// rateOf 依据上次累计值计算每秒增量,首次采集速率为 0
|
||||
func rateOf(prev ioSnapshot, in, out uint64, now time.Time) ioSnapshot {
|
||||
snap := ioSnapshot{in: in, out: out, at: now}
|
||||
if prev.at.IsZero() {
|
||||
return snap
|
||||
}
|
||||
|
||||
elapsed := now.Sub(prev.at).Seconds()
|
||||
if elapsed < 1 {
|
||||
elapsed = 1
|
||||
}
|
||||
if in >= prev.in {
|
||||
snap.inRate = uint64(float64(in-prev.in) / elapsed)
|
||||
}
|
||||
if out >= prev.out {
|
||||
snap.outRate = uint64(float64(out-prev.out) / elapsed)
|
||||
}
|
||||
|
||||
return snap
|
||||
}
|
||||
|
||||
func stateKey(ruleID uint, target string) string {
|
||||
return fmt.Sprintf("%d:%s", ruleID, target)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package biz
|
||||
|
||||
import "testing"
|
||||
|
||||
// 多域名证书的目标是逗号分隔的域名列表,用其中任一域名都应能命中规则
|
||||
func TestFilterTargetMatchesAnyCertDomain(t *testing.T) {
|
||||
metrics := []*AlertMetric{
|
||||
{Target: "a.example.com,b.example.com", Value: 5},
|
||||
{Target: "c.example.com", Value: 30},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
target string
|
||||
want int
|
||||
}{
|
||||
{"", 2},
|
||||
{"a.example.com", 1},
|
||||
{"b.example.com", 1},
|
||||
{"c.example.com", 1},
|
||||
{"a.example.com,b.example.com", 1},
|
||||
{"missing.example.com", 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
if got := filterTarget(c.target, metrics); len(got) != c.want {
|
||||
t.Fatalf("target %q matched %d metrics, want %d", c.target, len(got), c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+293
-8
@@ -1,8 +1,15 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/pkg/api"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -18,19 +25,297 @@ type App struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AppCustom 自定义编译参数(前置脚本在 configure 前执行,参数追加到 configure 末尾)
|
||||
type AppCustom struct {
|
||||
PreScript string `json:"pre_script"`
|
||||
Args string `json:"args"`
|
||||
}
|
||||
|
||||
// 支持自定义编译参数的应用与环境类型(源码编译类)
|
||||
var (
|
||||
customCompileApps = []string{"apache", "memcached", "nginx", "openresty", "pureftpd", "s3fs"}
|
||||
customCompileEnvTypes = []string{"php", "python"}
|
||||
)
|
||||
|
||||
// CustomCompileApp 判断应用是否支持自定义编译参数
|
||||
func CustomCompileApp(slug string) bool {
|
||||
return slices.Contains(customCompileApps, slug)
|
||||
}
|
||||
|
||||
// CustomCompileEnv 判断环境类型是否支持自定义编译参数
|
||||
func CustomCompileEnv(typ string) bool {
|
||||
return slices.Contains(customCompileEnvTypes, typ)
|
||||
}
|
||||
|
||||
type AppRepo interface {
|
||||
Categories() []types.LV
|
||||
All() api.Apps
|
||||
Get(slug string) (*api.App, error)
|
||||
UpdateExist(slug string) bool
|
||||
Installed() ([]*App, error)
|
||||
GetInstalled(slug string) (*App, error)
|
||||
GetInstalledAll(query string, cond ...string) ([]*App, error)
|
||||
GetHomeShow() ([]map[string]string, error)
|
||||
ListHomeShow() ([]*App, error)
|
||||
IsInstalled(query string, cond ...any) (bool, error)
|
||||
Install(channel, slug string) error
|
||||
UnInstall(slug string) error
|
||||
Update(slug string) error
|
||||
UpdateShow(slug string, show bool) error
|
||||
UpdateOrder(slugs []string) error
|
||||
DownloadCallback(slug string)
|
||||
CheckPanelVersion() error
|
||||
ResolveScript(item *api.App, matchChannel, action, execVersion string) (string, error)
|
||||
ExecScript(script string) error
|
||||
PreCheck(item *api.App, catalog api.Apps) error
|
||||
GetCustom(slug string) (*AppCustom, error)
|
||||
SaveCustom(slug string, custom *AppCustom) error
|
||||
}
|
||||
|
||||
type AppUsecase struct {
|
||||
repo AppRepo
|
||||
cache CacheRepo
|
||||
task TaskRepo
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewAppUsecase(t *gotext.Locale, appRepo AppRepo, cacheRepo CacheRepo, taskRepo TaskRepo) (*AppUsecase, error) {
|
||||
return &AppUsecase{
|
||||
repo: appRepo,
|
||||
cache: cacheRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Categories() []types.LV {
|
||||
cached, err := uc.cache.Get(CacheKeyCategories)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var categories api.Categories
|
||||
if err = json.Unmarshal([]byte(cached), &categories); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
slices.SortFunc(categories, func(a, b *api.Category) int {
|
||||
return a.Order - b.Order
|
||||
})
|
||||
|
||||
return lo.Map(categories, func(item *api.Category, _ int) types.LV {
|
||||
return types.LV{Label: item.Name, Value: item.Slug}
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) All() api.Apps {
|
||||
cached, err := uc.cache.Get(CacheKeyApps)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var apps api.Apps
|
||||
if err = json.Unmarshal([]byte(cached), &apps); err != nil {
|
||||
return nil
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Get(slug string) (*api.App, error) {
|
||||
for item := range slices.Values(uc.All()) {
|
||||
if item.Slug == slug {
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New(uc.t.Get("app %s not found", slug))
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) UpdateExist(slug string) bool {
|
||||
item, err := uc.Get(slug)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
installed, err := uc.repo.GetInstalled(slug)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for channel := range slices.Values(item.Channels) {
|
||||
if channel.Slug == installed.Channel {
|
||||
if channel.Version != installed.Version {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Installed() ([]*App, error) {
|
||||
return uc.repo.Installed()
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) GetInstalled(slug string) (*App, error) {
|
||||
return uc.repo.GetInstalled(slug)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) GetInstalledAll(query string, cond ...string) ([]*App, error) {
|
||||
return uc.repo.GetInstalledAll(query, cond...)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) GetHomeShow() ([]map[string]string, error) {
|
||||
apps, err := uc.repo.ListHomeShow()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtered := make([]map[string]string, 0)
|
||||
for item := range slices.Values(apps) {
|
||||
loaded, err := uc.Get(item.Slug)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, map[string]string{
|
||||
"name": loaded.Name,
|
||||
"description": loaded.Description,
|
||||
"slug": loaded.Slug,
|
||||
"version": item.Version,
|
||||
})
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) IsInstalled(query string, cond ...any) (bool, error) {
|
||||
return uc.repo.IsInstalled(query, cond...)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Install(channel, slug string) error {
|
||||
item, err := uc.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 恢复原编排:非法面板版本应在存在性检查前报错
|
||||
if err = uc.repo.CheckPanelVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if installed, _ := uc.repo.IsInstalled(slug); installed {
|
||||
return errors.New(uc.t.Get("app %s already installed", slug))
|
||||
}
|
||||
|
||||
script, err := uc.repo.ResolveScript(item, channel, "install", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.PreCheck(item, uc.All()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.repo.DownloadCallback(slug)
|
||||
|
||||
if app.IsCli {
|
||||
return uc.repo.ExecScript(script)
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "app:" + slug
|
||||
task.Name = uc.t.Get("Install app %s", item.Name)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = script
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) UnInstall(slug string) error {
|
||||
item, err := uc.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 恢复原编排:非法面板版本应在存在性检查前报错
|
||||
if err = uc.repo.CheckPanelVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if installed, _ := uc.repo.IsInstalled(slug); !installed {
|
||||
return errors.New(uc.t.Get("app %s not installed", item.Name))
|
||||
}
|
||||
installed, err := uc.repo.GetInstalled(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := uc.repo.ResolveScript(item, installed.Channel, "uninstall", installed.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.PreCheck(item, uc.All()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
return uc.repo.ExecScript(script)
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "app:" + slug
|
||||
task.Name = uc.t.Get("Uninstall app %s", item.Name)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = script
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Update(slug string) error {
|
||||
item, err := uc.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 恢复原编排:非法面板版本应在存在性检查前报错
|
||||
if err = uc.repo.CheckPanelVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if installed, _ := uc.repo.IsInstalled(slug); !installed {
|
||||
return errors.New(uc.t.Get("app %s not installed", item.Name))
|
||||
}
|
||||
installed, err := uc.repo.GetInstalled(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := uc.repo.ResolveScript(item, installed.Channel, "update", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.PreCheck(item, uc.All()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.repo.DownloadCallback(slug)
|
||||
|
||||
if app.IsCli {
|
||||
return uc.repo.ExecScript(script)
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "app:" + slug
|
||||
task.Name = uc.t.Get("Update app %s", item.Name)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = script
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) UpdateShow(slug string, show bool) error {
|
||||
return uc.repo.UpdateShow(slug, show)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) GetCustom(slug string) (*AppCustom, error) {
|
||||
return uc.repo.GetCustom(slug)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) SaveCustom(slug string, custom *AppCustom) error {
|
||||
return uc.repo.SaveCustom(slug, custom)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) UpdateOrder(slugs []string) error {
|
||||
return uc.repo.UpdateOrder(slugs)
|
||||
}
|
||||
|
||||
+114
-9
@@ -2,6 +2,10 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -9,25 +13,126 @@ import (
|
||||
type BackupType string
|
||||
|
||||
const (
|
||||
BackupTypePath BackupType = "path"
|
||||
BackupTypeWebsite BackupType = "website"
|
||||
BackupTypeMySQL BackupType = "mysql"
|
||||
BackupTypePostgres BackupType = "postgresql"
|
||||
BackupTypeRedis BackupType = "redis"
|
||||
BackupTypePanel BackupType = "panel"
|
||||
BackupTypePath BackupType = "path"
|
||||
BackupTypeWebsite BackupType = "website"
|
||||
BackupTypeMySQL BackupType = "mysql"
|
||||
BackupTypePostgres BackupType = "postgresql"
|
||||
BackupTypeClickHouse BackupType = "clickhouse"
|
||||
BackupTypeRedis BackupType = "redis"
|
||||
BackupTypeValkey BackupType = "valkey"
|
||||
BackupTypePanel BackupType = "panel"
|
||||
)
|
||||
|
||||
type BackupRepo interface {
|
||||
List(typ BackupType) ([]*types.BackupFile, error)
|
||||
GetStorage(id uint) (*BackupStorage, error)
|
||||
Create(ctx context.Context, typ BackupType, target string, account uint) error
|
||||
CreatePanel() error
|
||||
Delete(ctx context.Context, typ BackupType, name string) error
|
||||
Restore(ctx context.Context, typ BackupType, backup, target string) error
|
||||
Delete(typ BackupType, name string) error
|
||||
Restore(typ BackupType, backup, target string) error
|
||||
ClearExpired(path, prefix string, save uint) error
|
||||
ClearStorageExpired(account uint, typ BackupType, prefix string, save uint) error
|
||||
CutoffLog(path, target string) (string, error)
|
||||
CutoffUpload(account uint, typ BackupType, name string, files []string) error
|
||||
GetDefaultPath(typ BackupType) string
|
||||
FixPanel() error
|
||||
UpdatePanel(version, url, checksum string) error
|
||||
UpdatePanel(version, url, checksum string, progress func(string)) error
|
||||
}
|
||||
|
||||
type BackupUsecase struct {
|
||||
repo BackupRepo
|
||||
log *slog.Logger
|
||||
notify *NotifyUsecase
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewBackupUsecase(notifyUsecase *NotifyUsecase, t *gotext.Locale, log *slog.Logger, backupRepo BackupRepo) (*BackupUsecase, error) {
|
||||
return &BackupUsecase{
|
||||
repo: backupRepo,
|
||||
log: log,
|
||||
notify: notifyUsecase,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) List(typ BackupType) ([]*types.BackupFile, error) {
|
||||
return uc.repo.List(typ)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) Create(ctx context.Context, typ BackupType, target string, account uint) error {
|
||||
err := uc.repo.Create(ctx, typ, target, account)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 定时备份由 CLI 执行,命令返回即退出,异步通知来不及发出,必须同步发送
|
||||
if sendErr := uc.notify.SendEventSync(ctx, NotifyEventBackup, uc.t.Get("[AcePanel] Backup Failed"), NotifyBody(uc.t.Get("backup task failed"), [][2]string{
|
||||
{uc.t.Get("Type"), string(typ)},
|
||||
{uc.t.Get("Target"), target},
|
||||
{uc.t.Get("Error"), err.Error()},
|
||||
{uc.t.Get("Time"), time.Now().Format(time.DateTime)},
|
||||
})); sendErr != nil {
|
||||
uc.log.Warn("failed to send backup failure notification", slog.Any("err", sendErr))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) CreatePanel() error {
|
||||
return uc.repo.CreatePanel()
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) Delete(ctx context.Context, typ BackupType, name string) error {
|
||||
if err := uc.repo.Delete(typ, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("backup deleted", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.String("backup_type", string(typ)), slog.String("name", name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) Restore(ctx context.Context, typ BackupType, backup, target string) error {
|
||||
if err := uc.repo.Restore(typ, backup, target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("backup restored",
|
||||
slog.String("type", OperationTypeBackup),
|
||||
slog.Uint64("operator_id", operatorID(ctx)),
|
||||
slog.String("backup_type", string(typ)),
|
||||
slog.String("target", target),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) ClearExpired(path, prefix string, save uint) error {
|
||||
return uc.repo.ClearExpired(path, prefix, save)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) ClearStorageExpired(account uint, typ BackupType, prefix string, save uint) error {
|
||||
return uc.repo.ClearStorageExpired(account, typ, prefix, save)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) CutoffLog(path, target string) (string, error) {
|
||||
return uc.repo.CutoffLog(path, target)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) CutoffUpload(account uint, typ BackupType, name string, files []string) error {
|
||||
return uc.repo.CutoffUpload(account, typ, name, files)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) GetDefaultPath(typ BackupType) string {
|
||||
return uc.repo.GetDefaultPath(typ)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) FixPanel() error {
|
||||
return uc.repo.FixPanel()
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) UpdatePanel(version, url, checksum string, progress func(string)) error {
|
||||
return uc.repo.UpdatePanel(version, url, checksum, progress)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -129,9 +131,110 @@ func (r *BackupStorage) AfterFind(tx *gorm.DB) error {
|
||||
}
|
||||
|
||||
type BackupAccountRepo interface {
|
||||
List(page, limit uint) ([]*BackupStorage, int64, error)
|
||||
Get(id uint) (*BackupStorage, error)
|
||||
Create(ctx context.Context, req *request.BackupStorageCreate) (*BackupStorage, error)
|
||||
Update(ctx context.Context, req *request.BackupStorageUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
ListPaged(page, limit uint) ([]*BackupStorage, int64, error)
|
||||
GetByID(id uint) (*BackupStorage, error)
|
||||
Create(account *BackupStorage) error
|
||||
Update(account *BackupStorage) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type BackupAccountUsecase struct {
|
||||
repo BackupAccountRepo
|
||||
setting SettingRepo
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewBackupAccountUsecase(t *gotext.Locale, log *slog.Logger, backupAccountRepo BackupAccountRepo, settingRepo SettingRepo) (*BackupAccountUsecase, error) {
|
||||
return &BackupAccountUsecase{
|
||||
repo: backupAccountRepo,
|
||||
setting: settingRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) List(page, limit uint) ([]*BackupStorage, int64, error) {
|
||||
// 本地存储
|
||||
localStorage, err := uc.Get(0)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
dbAccounts, total, err := uc.repo.ListPaged(page, limit)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
accounts := make([]*BackupStorage, 0, len(dbAccounts)+1)
|
||||
if page == 1 {
|
||||
accounts = append(accounts, localStorage)
|
||||
}
|
||||
accounts = append(accounts, dbAccounts...)
|
||||
|
||||
return accounts, total + 1, nil
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) Get(id uint) (*BackupStorage, error) {
|
||||
if id == 0 {
|
||||
path, err := uc.setting.Get(SettingKeyBackupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BackupStorage{
|
||||
ID: 0,
|
||||
Type: BackupStorageTypeLocal,
|
||||
Name: uc.t.Get("Local Storage"),
|
||||
Info: types.BackupStorageInfo{
|
||||
Path: path,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return uc.repo.GetByID(id)
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) Create(ctx context.Context, req *request.BackupStorageCreate) (*BackupStorage, error) {
|
||||
account := &BackupStorage{
|
||||
Type: BackupStorageType(req.Type),
|
||||
Name: req.Name,
|
||||
Info: req.Info,
|
||||
}
|
||||
|
||||
if err := uc.repo.Create(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.log.Info("backup storage created", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(account.ID)), slog.String("account_type", req.Type), slog.String("name", req.Name))
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) Update(ctx context.Context, req *request.BackupStorageUpdate) error {
|
||||
account, err := uc.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account.Type = BackupStorageType(req.Type)
|
||||
account.Name = req.Name
|
||||
account.Info = req.Info
|
||||
|
||||
if err = uc.repo.Update(account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.log.Info("backup storage updated", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("account_type", req.Type))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) Delete(ctx context.Context, id uint) error {
|
||||
if err := uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.log.Info("backup storage deleted", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package biz
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewAlertUsecase, NewAppUsecase, NewBackupUsecase, NewBackupAccountUsecase,
|
||||
NewCacheUsecase, NewCertUsecase, NewCertAccountUsecase,
|
||||
NewCertDNSUsecase, NewContainerUsecase, NewContainerComposeUsecase,
|
||||
NewContainerImageUsecase, NewContainerNetworkUsecase, NewContainerVolumeUsecase,
|
||||
NewCronUsecase, NewDatabaseUsecase, NewDatabaseRedisUsecase,
|
||||
NewDatabaseElasticsearchUsecase, NewDatabaseServerUsecase, NewDatabaseUserUsecase,
|
||||
NewEnvironmentUsecase, NewFileShareUsecase, NewLogUsecase, NewMonitorUsecase,
|
||||
NewNotifyUsecase, NewProjectUsecase, NewSafeUsecase, NewScanEventUsecase,
|
||||
NewSettingUsecase, NewSSHUsecase, NewTamperUsecase, NewTaskUsecase,
|
||||
NewTemplateUsecase, NewUserUsecase, NewUserPasskeyUsecase,
|
||||
NewUserTokenUsecase, NewWebHookUsecase, NewWebsiteUsecase,
|
||||
NewWebsiteStatUsecase, NewWafUsecase,
|
||||
)
|
||||
+89
-5
@@ -1,6 +1,13 @@
|
||||
package biz
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/api"
|
||||
"github.com/acepanel/panel/v3/pkg/apploader"
|
||||
)
|
||||
|
||||
type CacheKey string
|
||||
|
||||
@@ -21,8 +28,85 @@ type Cache struct {
|
||||
type CacheRepo interface {
|
||||
Get(key CacheKey, defaultValue ...string) (string, error)
|
||||
Set(key CacheKey, value string) error
|
||||
UpdateCategories() error
|
||||
UpdateApps() error
|
||||
UpdateEnvironments() error
|
||||
UpdateTemplates() error
|
||||
FetchCategories() (*api.Categories, error)
|
||||
FetchApps() (*api.Apps, error)
|
||||
FetchEnvironments() (*api.Environments, error)
|
||||
FetchTemplates() (*api.Templates, error)
|
||||
}
|
||||
|
||||
type CacheUsecase struct {
|
||||
repo CacheRepo
|
||||
}
|
||||
|
||||
func NewCacheUsecase(repo CacheRepo) *CacheUsecase {
|
||||
return &CacheUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) Get(key CacheKey, defaultValue ...string) (string, error) {
|
||||
return uc.repo.Get(key, defaultValue...)
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) Set(key CacheKey, value string) error {
|
||||
return uc.repo.Set(key, value)
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) UpdateCategories() error {
|
||||
categories, err := uc.repo.FetchCategories()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(categories)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.repo.Set(CacheKeyCategories, string(encoded))
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) UpdateApps() error {
|
||||
remote, err := uc.repo.FetchApps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 去除本地不存在的应用
|
||||
*remote = slices.Clip(slices.DeleteFunc(*remote, func(item *api.App) bool {
|
||||
return !slices.Contains(apploader.SupportedSlugs(), item.Slug)
|
||||
}))
|
||||
|
||||
encoded, err := json.Marshal(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.repo.Set(CacheKeyApps, string(encoded))
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) UpdateEnvironments() error {
|
||||
environments, err := uc.repo.FetchEnvironments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(environments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.repo.Set(CacheKeyEnvironment, string(encoded))
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) UpdateTemplates() error {
|
||||
templates, err := uc.repo.FetchTemplates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(templates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.repo.Set(CacheKeyTemplates, string(encoded))
|
||||
}
|
||||
|
||||
+386
-13
@@ -2,12 +2,21 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
mholtacme "github.com/mholt/acmez/v3/acme"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/acme"
|
||||
pkgcert "github.com/acepanel/panel/v3/pkg/cert"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -37,16 +46,380 @@ type CertRepo interface {
|
||||
List(page, limit uint) ([]*types.CertList, int64, error)
|
||||
Get(id uint) (*Cert, error)
|
||||
GetByWebsite(WebsiteID uint) (*Cert, error)
|
||||
Upload(ctx context.Context, req *request.CertUpload) (*Cert, error)
|
||||
Create(ctx context.Context, req *request.CertCreate) (*Cert, error)
|
||||
Update(ctx context.Context, req *request.CertUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
ObtainAuto(id uint) (*acme.Certificate, error)
|
||||
ObtainAutoWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error)
|
||||
ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error)
|
||||
ObtainSelfSigned(id uint) error
|
||||
Renew(id uint) (*acme.Certificate, error)
|
||||
RenewWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error)
|
||||
RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error)
|
||||
Deploy(ID, WebsiteID uint, enableHTTPS bool) error
|
||||
Create(req *request.CertCreate) (*Cert, error)
|
||||
CreateUploaded(cert *Cert) error
|
||||
Update(req *request.CertUpdate) error
|
||||
Delete(id uint) error
|
||||
Save(cert *Cert) error
|
||||
GetClient(cert *Cert) (*acme.Client, error)
|
||||
GenerateSelfSigned(domains []string) ([]byte, []byte, error)
|
||||
RunScript(cert *Cert) error
|
||||
ObtainPanel(account *CertAccount, ips []string, webServer string) ([]byte, []byte, error)
|
||||
LoadWebsite(WebsiteID uint) (*Website, error)
|
||||
WriteCertFiles(cert *Cert, certPath, keyPath string) error
|
||||
EnableWebsiteSSL(website *Website, certPath, keyPath, webServer string, tlsVersions []string) error
|
||||
ReloadWebserver(webServer string) error
|
||||
}
|
||||
|
||||
type CertUsecase struct {
|
||||
repo CertRepo
|
||||
setting SettingRepo
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewCertUsecase(t *gotext.Locale, log *slog.Logger, certRepo CertRepo, settingRepo SettingRepo) (*CertUsecase, error) {
|
||||
return &CertUsecase{
|
||||
repo: certRepo,
|
||||
setting: settingRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
return uc.repo.List(page, limit)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Get(id uint) (*Cert, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) GetByWebsite(WebsiteID uint) (*Cert, error) {
|
||||
return uc.repo.GetByWebsite(WebsiteID)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Upload(ctx context.Context, req *request.CertUpload) (*Cert, error) {
|
||||
info, err := pkgcert.ParseCert([]byte(req.Cert))
|
||||
if err != nil {
|
||||
return nil, errors.New(uc.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err = pkgcert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return nil, errors.New(uc.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
|
||||
// 合并 DNSNames 和 IPAddresses
|
||||
domains := info.DNSNames
|
||||
for _, ip := range info.IPAddresses {
|
||||
domains = append(domains, ip.String())
|
||||
}
|
||||
|
||||
cert := &Cert{
|
||||
Type: "upload",
|
||||
Domains: domains,
|
||||
Cert: req.Cert,
|
||||
Key: req.Key,
|
||||
}
|
||||
if err = uc.repo.CreateUploaded(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert uploaded", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(cert.ID)))
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Create(ctx context.Context, req *request.CertCreate) (*Cert, error) {
|
||||
cert, err := uc.repo.Create(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert created", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(cert.ID)), slog.String("cert_type", req.Type))
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Update(ctx context.Context, req *request.CertUpdate) error {
|
||||
info, err := pkgcert.ParseCert([]byte(req.Cert))
|
||||
if err == nil && req.Type == "upload" {
|
||||
// 合并 DNSNames 和 IPAddresses
|
||||
req.Domains = info.DNSNames
|
||||
for _, ip := range info.IPAddresses {
|
||||
req.Domains = append(req.Domains, ip.String())
|
||||
}
|
||||
}
|
||||
if req.Type == "upload" && req.AutoRenewal {
|
||||
return errors.New(uc.t.Get("upload certificate cannot be set to auto renewal"))
|
||||
}
|
||||
|
||||
if err = uc.repo.Update(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert updated", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Delete(ctx context.Context, id uint) error {
|
||||
if err := uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert deleted", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
return uc.ObtainAutoWithProgressCallback(context.Background(), id, nil)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainAutoWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error) {
|
||||
report := func(msg string) {
|
||||
if progressCallback != nil {
|
||||
progressCallback(msg)
|
||||
}
|
||||
}
|
||||
|
||||
report(uc.t.Get("initializing ACME client"))
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := uc.repo.GetClient(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
|
||||
if cert.DNS != nil {
|
||||
client.UseDns(cert.DNS.Type, cert.DNS.Data, acme.DnsOption{
|
||||
Alias: cert.Alias,
|
||||
DnsServer: cert.DNS.Data.DnsServer,
|
||||
SkipVerify: cert.DNS.Data.SkipVerify,
|
||||
ProgressCallback: progressCallback,
|
||||
})
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New(uc.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
}
|
||||
hasWildcard := slices.ContainsFunc(cert.Domains, func(d string) bool {
|
||||
return strings.Contains(d, "*")
|
||||
})
|
||||
if hasWildcard {
|
||||
return nil, errors.New(uc.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
conf := fmt.Sprintf("%s/sites/%s/config/site/001-acme.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, webServer)
|
||||
}
|
||||
|
||||
report(uc.t.Get("issuing certificate, domains: %s", strings.Join(cert.Domains, ", ")))
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
ssl, err := client.ObtainCertificate(ctx, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report(uc.t.Get("obtaining and saving certificate"))
|
||||
cert.RenewalInfo = *ssl.RenewalInfo
|
||||
cert.CertURL = ssl.URL
|
||||
cert.Cert = string(ssl.ChainPEM)
|
||||
cert.Key = string(ssl.PrivateKey)
|
||||
if err = uc.repo.Save(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
report(uc.t.Get("deploying certificate to website"))
|
||||
return &ssl, uc.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
if err = uc.repo.RunScript(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error) {
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
return uc.repo.ObtainPanel(account, ips, webServer)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainSelfSigned(id uint) error {
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crt, key, err := uc.repo.GenerateSelfSigned(cert.Domains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert.Cert = string(crt)
|
||||
cert.Key = string(key)
|
||||
if err = uc.repo.Save(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return uc.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
if err = uc.repo.RunScript(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Renew(id uint) (*acme.Certificate, error) {
|
||||
return uc.RenewWithProgressCallback(context.Background(), id, nil)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) RenewWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error) {
|
||||
report := func(msg string) {
|
||||
if progressCallback != nil {
|
||||
progressCallback(msg)
|
||||
}
|
||||
}
|
||||
|
||||
report(uc.t.Get("preparing renewal"))
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := uc.repo.GetClient(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cert.CertURL == "" {
|
||||
return nil, errors.New(uc.t.Get("this certificate has not been obtained successfully and cannot be renewed"))
|
||||
}
|
||||
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
|
||||
if cert.DNS != nil {
|
||||
client.UseDns(cert.DNS.Type, cert.DNS.Data, acme.DnsOption{
|
||||
Alias: cert.Alias,
|
||||
DnsServer: cert.DNS.Data.DnsServer,
|
||||
SkipVerify: cert.DNS.Data.SkipVerify,
|
||||
ProgressCallback: progressCallback,
|
||||
})
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New(uc.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
} else {
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New(uc.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/sites/%s/config/site/001-acme.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, webServer)
|
||||
}
|
||||
}
|
||||
|
||||
report(uc.t.Get("renewing certificate, domains: %s", strings.Join(cert.Domains, ", ")))
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
ssl, err := client.RenewCertificate(ctx, cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
// 续签失败,尝试重签
|
||||
report(uc.t.Get("renewal failed, attempting re-issuance"))
|
||||
ssl, err = client.ObtainCertificate(ctx, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
report(uc.t.Get("obtaining and saving certificate"))
|
||||
cert.RenewalInfo = *ssl.RenewalInfo
|
||||
cert.CertURL = ssl.URL
|
||||
cert.Cert = string(ssl.ChainPEM)
|
||||
cert.Key = string(ssl.PrivateKey)
|
||||
if err = uc.repo.Save(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
report(uc.t.Get("deploying certificate to website"))
|
||||
return &ssl, uc.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error) {
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
client, err := uc.repo.GetClient(cert)
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
crt, err := pkgcert.ParseCert([]byte(cert.Cert))
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
renewInfo, err := client.GetRenewalInfo(ctx, crt)
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
cert.RenewalInfo = renewInfo
|
||||
if err = uc.repo.Save(cert); err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
return renewInfo, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Deploy(ID, WebsiteID uint, enableHTTPS bool) error {
|
||||
cert, err := uc.repo.Get(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cert.Cert == "" || cert.Key == "" {
|
||||
return errors.New(uc.t.Get("this certificate has not been obtained successfully and cannot be deployed"))
|
||||
}
|
||||
|
||||
website, err := uc.repo.LoadWebsite(WebsiteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||
certPath := filepath.Join(configDir, "fullchain.pem")
|
||||
keyPath := filepath.Join(configDir, "private.key")
|
||||
if err = uc.repo.WriteCertFiles(cert, certPath, keyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 开启 HTTPS
|
||||
if enableHTTPS && !website.SSL {
|
||||
// 原 getVhost 首步读 webserver 设置并传播错误,保持该语义
|
||||
webServer, err := uc.setting.Get(SettingKeyWebserver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsVersions, _ := uc.setting.GetSlice(SettingKeyWebsiteTLSVersions)
|
||||
if err = uc.repo.EnableWebsiteSSL(website, certPath, keyPath, webServer, tlsVersions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
return uc.repo.ReloadWebserver(webServer)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Save(cert *Cert) error {
|
||||
return uc.repo.Save(cert)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,15 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/acme"
|
||||
"github.com/acepanel/panel/v3/pkg/cert"
|
||||
)
|
||||
|
||||
type CertAccount struct {
|
||||
@@ -23,9 +29,188 @@ type CertAccount struct {
|
||||
|
||||
type CertAccountRepo interface {
|
||||
List(page, limit uint) ([]*CertAccount, int64, error)
|
||||
GetDefault(userID uint) (*CertAccount, error)
|
||||
Get(id uint) (*CertAccount, error)
|
||||
Create(ctx context.Context, req *request.CertAccountCreate) (*CertAccount, error)
|
||||
Update(ctx context.Context, req *request.CertAccountUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
GetByCAEmail(ca, email string) (*CertAccount, error)
|
||||
Create(account *CertAccount) error
|
||||
Save(account *CertAccount) error
|
||||
Delete(id uint) error
|
||||
GetGoogleEAB() (*acme.EAB, error)
|
||||
GetZeroSSLEAB(email string) (*acme.EAB, error)
|
||||
RegisterAccount(email, ca string, eab *acme.EAB, keyType acme.KeyType) (*acme.Client, error)
|
||||
}
|
||||
|
||||
type CertAccountUsecase struct {
|
||||
repo CertAccountRepo
|
||||
userRepo UserRepo
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewCertAccountUsecase(t *gotext.Locale, log *slog.Logger, certAccountRepo CertAccountRepo, userRepo UserRepo) (*CertAccountUsecase, error) {
|
||||
return &CertAccountUsecase{
|
||||
repo: certAccountRepo,
|
||||
userRepo: userRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *CertAccountUsecase) List(page, limit uint) ([]*CertAccount, int64, error) {
|
||||
return uc.repo.List(page, limit)
|
||||
}
|
||||
|
||||
func (uc *CertAccountUsecase) GetDefault(userID uint) (*CertAccount, error) {
|
||||
user, err := uc.userRepo.Get(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
account, err := uc.repo.GetByCAEmail("letsencrypt", user.Email)
|
||||
if err == nil {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
req := &request.CertAccountCreate{
|
||||
CA: "letsencrypt",
|
||||
Email: user.Email,
|
||||
KeyType: string(acme.KeyEC256),
|
||||
}
|
||||
|
||||
return uc.Create(context.Background(), req)
|
||||
}
|
||||
|
||||
func (uc *CertAccountUsecase) Get(id uint) (*CertAccount, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *CertAccountUsecase) Create(ctx context.Context, req *request.CertAccountCreate) (*CertAccount, error) {
|
||||
account := new(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 "googlecn":
|
||||
eab, eabErr := uc.repo.GetGoogleEAB()
|
||||
if eabErr != nil {
|
||||
return nil, eabErr
|
||||
}
|
||||
account.Kid = eab.KeyID
|
||||
account.HmacEncoded = eab.MACKey
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CAGoogleCN, eab, acme.KeyType(account.KeyType))
|
||||
case "google":
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
|
||||
case "letsencrypt":
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType))
|
||||
case "litessl":
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CALiteSSL, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
|
||||
case "zerossl":
|
||||
eab, eabErr := uc.repo.GetZeroSSLEAB(account.Email)
|
||||
if eabErr != nil {
|
||||
return nil, eabErr
|
||||
}
|
||||
account.Kid = eab.KeyID
|
||||
account.HmacEncoded = eab.MACKey
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType))
|
||||
case "sslcom":
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
|
||||
default:
|
||||
return nil, errors.New(uc.t.Get("unsupported CA"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New(uc.t.Get("failed to register account: %v", err))
|
||||
}
|
||||
|
||||
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, errors.New(uc.t.Get("failed to get private key"))
|
||||
}
|
||||
account.PrivateKey = string(privateKey)
|
||||
|
||||
if err = uc.repo.Create(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert account created", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(account.ID)), slog.String("ca", req.CA), slog.String("email", req.Email))
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (uc *CertAccountUsecase) Update(ctx context.Context, req *request.CertAccountUpdate) error {
|
||||
account, err := uc.repo.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 "googlecn":
|
||||
eab, eabErr := uc.repo.GetGoogleEAB()
|
||||
if eabErr != nil {
|
||||
return eabErr
|
||||
}
|
||||
account.Kid = eab.KeyID
|
||||
account.HmacEncoded = eab.MACKey
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CAGoogleCN, eab, acme.KeyType(account.KeyType))
|
||||
case "google":
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
|
||||
case "letsencrypt":
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType))
|
||||
case "litessl":
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CALiteSSL, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
|
||||
case "zerossl":
|
||||
eab, eabErr := uc.repo.GetZeroSSLEAB(account.Email)
|
||||
if eabErr != nil {
|
||||
return eabErr
|
||||
}
|
||||
account.Kid = eab.KeyID
|
||||
account.HmacEncoded = eab.MACKey
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType))
|
||||
case "sslcom":
|
||||
client, err = uc.repo.RegisterAccount(account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
|
||||
default:
|
||||
return errors.New(uc.t.Get("unsupported CA"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.New(uc.t.Get("failed to register account: %v", err))
|
||||
}
|
||||
|
||||
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
|
||||
if err != nil {
|
||||
return errors.New(uc.t.Get("failed to get private key: %v", err))
|
||||
}
|
||||
account.PrivateKey = string(privateKey)
|
||||
|
||||
if err = uc.repo.Save(account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert account updated", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("ca", req.CA))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CertAccountUsecase) Delete(ctx context.Context, id uint) error {
|
||||
if err := uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert account deleted", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/acme"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,63 @@ type CertDNS struct {
|
||||
type CertDNSRepo interface {
|
||||
List(page, limit uint) ([]*CertDNS, int64, error)
|
||||
Get(id uint) (*CertDNS, error)
|
||||
Create(ctx context.Context, req *request.CertDNSCreate) (*CertDNS, error)
|
||||
Update(ctx context.Context, req *request.CertDNSUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
Create(req *request.CertDNSCreate) (*CertDNS, error)
|
||||
Update(req *request.CertDNSUpdate) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type CertDNSUsecase struct {
|
||||
repo CertDNSRepo
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewCertDNSUsecase(repo CertDNSRepo, log *slog.Logger) *CertDNSUsecase {
|
||||
return &CertDNSUsecase{repo: repo, log: log}
|
||||
}
|
||||
|
||||
func (uc *CertDNSUsecase) List(page, limit uint) ([]*CertDNS, int64, error) {
|
||||
return uc.repo.List(page, limit)
|
||||
}
|
||||
|
||||
func (uc *CertDNSUsecase) Get(id uint) (*CertDNS, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *CertDNSUsecase) Create(ctx context.Context, req *request.CertDNSCreate) (*CertDNS, error) {
|
||||
certDNS, err := uc.repo.Create(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert dns created", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(certDNS.ID)), slog.String("name", req.Name))
|
||||
|
||||
return certDNS, nil
|
||||
}
|
||||
|
||||
func (uc *CertDNSUsecase) Update(ctx context.Context, req *request.CertDNSUpdate) error {
|
||||
if err := uc.repo.Update(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert dns updated", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", req.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CertDNSUsecase) Delete(ctx context.Context, id uint) error {
|
||||
certDNS, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert dns deleted", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", certDNS.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+114
-14
@@ -1,22 +1,122 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerRepo interface {
|
||||
ListAll() ([]types.Container, error)
|
||||
ListByName(name 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
|
||||
Kill(id string) error
|
||||
Rename(id string, newName string) error
|
||||
Logs(id string) (string, error)
|
||||
Prune() error
|
||||
ListAll(sock string) ([]types.Container, error)
|
||||
Inspect(sock string, id string) (any, error)
|
||||
Create(sock string, req *request.ContainerCreate) (string, error)
|
||||
Remove(sock string, id string) error
|
||||
Start(sock string, id string) error
|
||||
Stop(sock string, id string) error
|
||||
Restart(sock string, id string) error
|
||||
Pause(sock string, id string) error
|
||||
Unpause(sock string, id string) error
|
||||
Kill(sock string, id string) error
|
||||
Rename(sock string, id string, newName string) error
|
||||
Logs(sock string, id string, tail int) (string, error)
|
||||
Prune(sock string) error
|
||||
}
|
||||
|
||||
type ContainerUsecase struct {
|
||||
repo ContainerRepo
|
||||
setting SettingRepo
|
||||
}
|
||||
|
||||
func NewContainerUsecase(repo ContainerRepo, setting SettingRepo) *ContainerUsecase {
|
||||
return &ContainerUsecase{repo: repo, setting: setting}
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) ListAll() ([]types.Container, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.ListAll(sock)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) ListByName(name string) ([]types.Container, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
containers, err := uc.repo.ListAll(sock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containers = slices.DeleteFunc(containers, func(item types.Container) bool {
|
||||
return !strings.Contains(item.Name, name)
|
||||
})
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Inspect(id string) (any, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Inspect(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Create(req *request.ContainerCreate) (string, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Create(sock, req)
|
||||
}
|
||||
|
||||
// Update 删除旧容器后按新配置重建同名容器
|
||||
func (uc *ContainerUsecase) Update(id string, req *request.ContainerCreate) (string, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
if err := uc.repo.Remove(sock, id); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return uc.repo.Create(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Remove(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Remove(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Start(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Start(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Stop(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Stop(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Restart(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Restart(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Pause(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Pause(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Unpause(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Unpause(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Kill(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Kill(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Rename(id string, newName string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Rename(sock, id, newName)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Logs(id string, tail int) (string, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Logs(sock, id, tail)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Prune() error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Prune(sock)
|
||||
}
|
||||
|
||||
@@ -9,5 +9,44 @@ type ContainerComposeRepo interface {
|
||||
Update(name, compose string, envs []types.KV) error
|
||||
Up(name string, force bool) error
|
||||
Down(name string) error
|
||||
Remove(name string) error
|
||||
RemoveDir(name string) error
|
||||
}
|
||||
|
||||
type ContainerComposeUsecase struct {
|
||||
repo ContainerComposeRepo
|
||||
}
|
||||
|
||||
func NewContainerComposeUsecase(repo ContainerComposeRepo) *ContainerComposeUsecase {
|
||||
return &ContainerComposeUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *ContainerComposeUsecase) List() ([]types.ContainerCompose, error) {
|
||||
return uc.repo.List()
|
||||
}
|
||||
|
||||
func (uc *ContainerComposeUsecase) Get(name string) (string, []types.KV, error) {
|
||||
return uc.repo.Get(name)
|
||||
}
|
||||
|
||||
func (uc *ContainerComposeUsecase) Create(name, compose string, envs []types.KV) error {
|
||||
return uc.repo.Create(name, compose, envs)
|
||||
}
|
||||
|
||||
func (uc *ContainerComposeUsecase) Update(name, compose string, envs []types.KV) error {
|
||||
return uc.repo.Update(name, compose, envs)
|
||||
}
|
||||
|
||||
func (uc *ContainerComposeUsecase) Up(name string, force bool) error {
|
||||
return uc.repo.Up(name, force)
|
||||
}
|
||||
|
||||
func (uc *ContainerComposeUsecase) Down(name string) error {
|
||||
return uc.repo.Down(name)
|
||||
}
|
||||
|
||||
func (uc *ContainerComposeUsecase) Remove(name string) error {
|
||||
if err := uc.repo.Down(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.repo.RemoveDir(name)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,48 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerImageRepo interface {
|
||||
List() ([]types.ContainerImage, error)
|
||||
Exist(name string) (bool, error)
|
||||
Pull(req *request.ContainerImagePull) error
|
||||
Remove(id string) error
|
||||
Prune() error
|
||||
List(sock string) ([]types.ContainerImage, error)
|
||||
Exist(sock string, name string) (bool, error)
|
||||
Pull(sock string, req *request.ContainerImagePull) error
|
||||
Remove(sock string, id string) error
|
||||
Prune(sock string) error
|
||||
}
|
||||
|
||||
type ContainerImageUsecase struct {
|
||||
repo ContainerImageRepo
|
||||
setting SettingRepo
|
||||
}
|
||||
|
||||
func NewContainerImageUsecase(repo ContainerImageRepo, setting SettingRepo) *ContainerImageUsecase {
|
||||
return &ContainerImageUsecase{repo: repo, setting: setting}
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) List() ([]types.ContainerImage, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.List(sock)
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) Exist(name string) (bool, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Exist(sock, name)
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) Pull(req *request.ContainerImagePull) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Pull(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) Remove(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Remove(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) Prune() error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Prune(sock)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerNetworkRepo interface {
|
||||
List() ([]types.ContainerNetwork, error)
|
||||
Create(req *request.ContainerNetworkCreate) (string, error)
|
||||
Remove(id string) error
|
||||
Prune() error
|
||||
List(sock string) ([]types.ContainerNetwork, error)
|
||||
Create(sock string, req *request.ContainerNetworkCreate) (string, error)
|
||||
Remove(sock string, id string) error
|
||||
Prune(sock string) error
|
||||
}
|
||||
|
||||
type ContainerNetworkUsecase struct {
|
||||
repo ContainerNetworkRepo
|
||||
setting SettingRepo
|
||||
}
|
||||
|
||||
func NewContainerNetworkUsecase(repo ContainerNetworkRepo, setting SettingRepo) *ContainerNetworkUsecase {
|
||||
return &ContainerNetworkUsecase{repo: repo, setting: setting}
|
||||
}
|
||||
|
||||
func (uc *ContainerNetworkUsecase) List() ([]types.ContainerNetwork, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.List(sock)
|
||||
}
|
||||
|
||||
func (uc *ContainerNetworkUsecase) Create(req *request.ContainerNetworkCreate) (string, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Create(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerNetworkUsecase) Remove(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Remove(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerNetworkUsecase) Prune() error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Prune(sock)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerVolumeRepo interface {
|
||||
List() ([]types.ContainerVolume, error)
|
||||
Create(req *request.ContainerVolumeCreate) (string, error)
|
||||
Remove(id string) error
|
||||
Prune() error
|
||||
List(sock string) ([]types.ContainerVolume, error)
|
||||
Create(sock string, req *request.ContainerVolumeCreate) (string, error)
|
||||
Remove(sock string, id string) error
|
||||
Prune(sock string) error
|
||||
}
|
||||
|
||||
type ContainerVolumeUsecase struct {
|
||||
repo ContainerVolumeRepo
|
||||
setting SettingRepo
|
||||
}
|
||||
|
||||
func NewContainerVolumeUsecase(repo ContainerVolumeRepo, setting SettingRepo) *ContainerVolumeUsecase {
|
||||
return &ContainerVolumeUsecase{repo: repo, setting: setting}
|
||||
}
|
||||
|
||||
func (uc *ContainerVolumeUsecase) List() ([]types.ContainerVolume, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.List(sock)
|
||||
}
|
||||
|
||||
func (uc *ContainerVolumeUsecase) Create(req *request.ContainerVolumeCreate) (string, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Create(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerVolumeUsecase) Remove(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Remove(sock, id)
|
||||
}
|
||||
|
||||
func (uc *ContainerVolumeUsecase) Prune() error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Prune(sock)
|
||||
}
|
||||
|
||||
+181
-5
@@ -2,9 +2,10 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -25,8 +26,183 @@ type CronRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint) ([]*Cron, int64, error)
|
||||
Get(id uint) (*Cron, error)
|
||||
Create(ctx context.Context, req *request.CronCreate) error
|
||||
Update(ctx context.Context, req *request.CronUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
Status(id uint, status bool) error
|
||||
Create(cron *Cron) error
|
||||
Save(cron *Cron) error
|
||||
Delete(cron *Cron) error
|
||||
GenerateScript(typ string, config types.CronConfig, rawScript string) string
|
||||
WriteNewScript(script string) (string, string, error)
|
||||
WriteScript(path, script string) error
|
||||
Dos2Unix(path string) error
|
||||
AddToSystem(cron *Cron) error
|
||||
DeleteFromSystem(cron *Cron) error
|
||||
RemoveScriptFiles(shellPath string) error
|
||||
}
|
||||
|
||||
// CronUsecase 计划任务业务逻辑
|
||||
type CronUsecase struct {
|
||||
repo CronRepo
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewCronUsecase(repo CronRepo, log *slog.Logger) *CronUsecase {
|
||||
return &CronUsecase{repo: repo, log: log}
|
||||
}
|
||||
|
||||
func (uc *CronUsecase) Count() (int64, error) {
|
||||
return uc.repo.Count()
|
||||
}
|
||||
|
||||
func (uc *CronUsecase) List(page, limit uint) ([]*Cron, int64, error) {
|
||||
return uc.repo.List(page, limit)
|
||||
}
|
||||
|
||||
func (uc *CronUsecase) Get(id uint) (*Cron, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *CronUsecase) Create(ctx context.Context, req *request.CronCreate) error {
|
||||
config := types.CronConfig{
|
||||
Type: req.SubType,
|
||||
Flock: req.Flock,
|
||||
Targets: req.Targets,
|
||||
Storage: req.Storage,
|
||||
Keep: req.Keep,
|
||||
URL: req.URL,
|
||||
Method: req.Method,
|
||||
Headers: req.Headers,
|
||||
Body: req.Body,
|
||||
Timeout: req.Timeout,
|
||||
Insecure: req.Insecure,
|
||||
Retries: req.Retries,
|
||||
}
|
||||
script := uc.repo.GenerateScript(req.Type, config, req.Script)
|
||||
|
||||
shellPath, logPath, err := uc.repo.WriteNewScript(script)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cron := new(Cron)
|
||||
cron.Name = req.Name
|
||||
cron.Type = req.Type
|
||||
cron.Status = true
|
||||
cron.Time = req.Time
|
||||
cron.Shell = shellPath
|
||||
cron.Log = logPath
|
||||
cron.Config = config
|
||||
|
||||
if err := uc.repo.Create(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := uc.repo.AddToSystem(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cron created", slog.String("type", OperationTypeCron), slog.Uint64("operator_id", operatorID(ctx)), slog.String("name", req.Name), slog.String("cron_type", req.Type))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CronUsecase) Update(ctx context.Context, req *request.CronUpdate) error {
|
||||
cron, err := uc.repo.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cron.Time = req.Time
|
||||
cron.Name = req.Name
|
||||
|
||||
// 根据类型重新生成脚本
|
||||
if req.Type != "shell" {
|
||||
config := types.CronConfig{
|
||||
Type: req.SubType,
|
||||
Flock: req.Flock,
|
||||
Targets: req.Targets,
|
||||
Storage: req.Storage,
|
||||
Keep: req.Keep,
|
||||
URL: req.URL,
|
||||
Method: req.Method,
|
||||
Headers: req.Headers,
|
||||
Body: req.Body,
|
||||
Timeout: req.Timeout,
|
||||
Insecure: req.Insecure,
|
||||
Retries: req.Retries,
|
||||
}
|
||||
cron.Config = config
|
||||
script := uc.repo.GenerateScript(req.Type, config, "")
|
||||
if err = uc.repo.WriteScript(cron.Shell, script); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
cron.Config.Flock = req.Flock
|
||||
if err = uc.repo.WriteScript(cron.Shell, req.Script); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = uc.repo.Save(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.Dos2Unix(cron.Shell); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.DeleteFromSystem(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
if cron.Status {
|
||||
if err = uc.repo.AddToSystem(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cron updated", slog.String("type", OperationTypeCron), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", cron.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CronUsecase) Delete(ctx context.Context, id uint) error {
|
||||
cron, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.DeleteFromSystem(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = uc.repo.RemoveScriptFiles(cron.Shell); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.Delete(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cron deleted", slog.String("type", OperationTypeCron), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", cron.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CronUsecase) Status(id uint, status bool) error {
|
||||
cron, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.DeleteFromSystem(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
if status {
|
||||
if err = uc.repo.AddToSystem(cron); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cron.Status = status
|
||||
|
||||
return uc.repo.Save(cron)
|
||||
}
|
||||
|
||||
+226
-5
@@ -2,8 +2,13 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
type DatabaseType string
|
||||
@@ -28,8 +33,224 @@ type Database struct {
|
||||
}
|
||||
|
||||
type DatabaseRepo interface {
|
||||
List(page, limit uint, typ string) ([]*Database, int64, error)
|
||||
Create(ctx context.Context, req *request.DatabaseCreate) error
|
||||
Delete(ctx context.Context, serverID uint, name string) error
|
||||
Comment(req *request.DatabaseComment) error
|
||||
ListServers(typ string) ([]*DatabaseServer, error)
|
||||
DatabasesOf(ctx context.Context, server *DatabaseServer) ([]*Database, error)
|
||||
Operator(ctx context.Context, server *DatabaseServer) (db.Operator, error)
|
||||
Mongo(ctx context.Context, server *DatabaseServer) (*db.MongoDB, error)
|
||||
}
|
||||
|
||||
// DatabaseUsecase 数据库业务用例
|
||||
type DatabaseUsecase struct {
|
||||
repo DatabaseRepo
|
||||
server DatabaseServerRepo
|
||||
user *DatabaseUserUsecase
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseUsecase(databaseUserUsecase *DatabaseUserUsecase, t *gotext.Locale, log *slog.Logger, databaseRepo DatabaseRepo, databaseServerRepo DatabaseServerRepo) (*DatabaseUsecase, error) {
|
||||
return &DatabaseUsecase{
|
||||
repo: databaseRepo,
|
||||
server: databaseServerRepo,
|
||||
user: databaseUserUsecase,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseUsecase) List(ctx context.Context, page, limit uint, typ string) ([]*Database, int64, error) {
|
||||
servers, err := uc.repo.ListServers(typ)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
database := make([]*Database, 0)
|
||||
for _, server := range servers {
|
||||
databases, err := uc.repo.DatabasesOf(ctx, server)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
database = append(database, databases...)
|
||||
}
|
||||
|
||||
if len(database) < int((page-1)*limit) {
|
||||
return []*Database{}, int64(len(database)), nil
|
||||
}
|
||||
|
||||
return database[(page-1)*limit:], int64(len(database)), nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseUsecase) Create(ctx context.Context, req *request.DatabaseCreate) error {
|
||||
server, err := uc.server.Get(ctx, req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// MongoDB 独立处理,不走 Operator 接口
|
||||
if server.Type == DatabaseTypeMongoDB {
|
||||
mongo, mongoErr := uc.repo.Mongo(ctx, server)
|
||||
if mongoErr != nil {
|
||||
return mongoErr
|
||||
}
|
||||
defer mongo.Close()
|
||||
if mongoErr = mongo.DatabaseCreate(req.Name); mongoErr != nil {
|
||||
return mongoErr
|
||||
}
|
||||
uc.log.Info("database created", slog.String("type", OperationTypeDatabase), slog.Uint64("operator_id", operatorID(ctx)), slog.String("name", req.Name), slog.Uint64("server_id", uint64(req.ServerID)))
|
||||
return nil
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(ctx, server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer operator.Close()
|
||||
|
||||
switch server.Type {
|
||||
case DatabaseTypeMysql:
|
||||
if req.CreateUser {
|
||||
if err = uc.user.Create(ctx, &request.DatabaseUserCreate{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Host: req.Host,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = operator.DatabaseCreate(req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Username != "" {
|
||||
// 授权已有用户时按其实际 host 授权,MySQL 8 起 GRANT 不再隐式创建用户
|
||||
hosts := []string{req.Host}
|
||||
if !req.CreateUser {
|
||||
if hosts = uc.mysqlUserHosts(operator, req.Username); len(hosts) == 0 {
|
||||
return errors.New(uc.t.Get("database user %s does not exist", req.Username))
|
||||
}
|
||||
}
|
||||
for _, host := range hosts {
|
||||
if err = operator.PrivilegesGrant(req.Username, req.Name, host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case DatabaseTypePostgresql:
|
||||
if req.CreateUser {
|
||||
if err = uc.user.Create(ctx, &request.DatabaseUserCreate{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Host: req.Host,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = operator.DatabaseCreate(req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Username != "" {
|
||||
if err = operator.PrivilegesGrant(req.Username, req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = operator.(*db.Postgres).DatabaseComment(req.Name, req.Comment); err != nil {
|
||||
return err
|
||||
}
|
||||
case DatabaseTypeClickHouse:
|
||||
if req.CreateUser {
|
||||
if err = uc.user.Create(ctx, &request.DatabaseUserCreate{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = operator.DatabaseCreate(req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Username != "" {
|
||||
if err = operator.PrivilegesGrant(req.Username, req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("database created", slog.String("type", OperationTypeDatabase), slog.Uint64("operator_id", operatorID(ctx)), slog.String("name", req.Name), slog.Uint64("server_id", uint64(req.ServerID)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mysqlUserHosts 查询 MySQL 用户实际存在的 host 列表
|
||||
func (uc *DatabaseUsecase) mysqlUserHosts(operator db.Operator, user string) []string {
|
||||
rows, err := operator.Query("SELECT host FROM mysql.user WHERE user = ?", user)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var hosts []string
|
||||
for rows.Next() {
|
||||
var host string
|
||||
if rows.Scan(&host) == nil {
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func (uc *DatabaseUsecase) Delete(ctx context.Context, serverID uint, name string) error {
|
||||
server, err := uc.server.Get(ctx, serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case DatabaseTypeMongoDB:
|
||||
mongo, mongoErr := uc.repo.Mongo(ctx, server)
|
||||
if mongoErr != nil {
|
||||
return mongoErr
|
||||
}
|
||||
defer mongo.Close()
|
||||
if mongoErr = mongo.DatabaseDrop(name); mongoErr != nil {
|
||||
return mongoErr
|
||||
}
|
||||
case DatabaseTypeSQLite:
|
||||
return errors.New(uc.t.Get("sqlite does not support dropping tables from here"))
|
||||
default:
|
||||
operator, opErr := uc.repo.Operator(ctx, server)
|
||||
if opErr != nil {
|
||||
return opErr
|
||||
}
|
||||
defer operator.Close()
|
||||
if opErr = operator.DatabaseDrop(name); opErr != nil {
|
||||
return opErr
|
||||
}
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("database deleted", slog.String("type", OperationTypeDatabase), slog.Uint64("operator_id", operatorID(ctx)), slog.String("name", name), slog.Uint64("server_id", uint64(serverID)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseUsecase) Comment(ctx context.Context, req *request.DatabaseComment) error {
|
||||
server, err := uc.server.Get(ctx, req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case DatabaseTypePostgresql:
|
||||
operator, opErr := uc.repo.Operator(ctx, server)
|
||||
if opErr != nil {
|
||||
return opErr
|
||||
}
|
||||
defer operator.Close()
|
||||
return operator.(*db.Postgres).DatabaseComment(req.Name, req.Comment)
|
||||
default:
|
||||
return errors.New(uc.t.Get("%s does not support database comment", server.Type))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,55 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"context"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
type DatabaseElasticsearchRepo interface {
|
||||
Indices(req *request.DatabaseESIndices) ([]db.ESIndex, error)
|
||||
IndexCreate(req *request.DatabaseESIndexCreate) error
|
||||
IndexDelete(req *request.DatabaseESIndexDelete) error
|
||||
Data(req *request.DatabaseESData) ([]db.ESDocument, int64, error)
|
||||
DocumentGet(req *request.DatabaseESDocumentGet) (*db.ESDocument, error)
|
||||
DocumentSet(req *request.DatabaseESDocumentSet) error
|
||||
DocumentDelete(req *request.DatabaseESDocumentDelete) error
|
||||
Indices(ctx context.Context, req *request.DatabaseESIndices) ([]db.ESIndex, error)
|
||||
IndexCreate(ctx context.Context, req *request.DatabaseESIndexCreate) error
|
||||
IndexDelete(ctx context.Context, req *request.DatabaseESIndexDelete) error
|
||||
Data(ctx context.Context, req *request.DatabaseESData) ([]db.ESDocument, int64, error)
|
||||
DocumentGet(ctx context.Context, req *request.DatabaseESDocumentGet) (*db.ESDocument, error)
|
||||
DocumentSet(ctx context.Context, req *request.DatabaseESDocumentSet) error
|
||||
DocumentDelete(ctx context.Context, req *request.DatabaseESDocumentDelete) error
|
||||
}
|
||||
|
||||
// DatabaseElasticsearchUsecase Elasticsearch 业务用例
|
||||
type DatabaseElasticsearchUsecase struct {
|
||||
repo DatabaseElasticsearchRepo
|
||||
}
|
||||
|
||||
func NewDatabaseElasticsearchUsecase(repo DatabaseElasticsearchRepo) *DatabaseElasticsearchUsecase {
|
||||
return &DatabaseElasticsearchUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) Indices(ctx context.Context, req *request.DatabaseESIndices) ([]db.ESIndex, error) {
|
||||
return uc.repo.Indices(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) IndexCreate(ctx context.Context, req *request.DatabaseESIndexCreate) error {
|
||||
return uc.repo.IndexCreate(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) IndexDelete(ctx context.Context, req *request.DatabaseESIndexDelete) error {
|
||||
return uc.repo.IndexDelete(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) Data(ctx context.Context, req *request.DatabaseESData) ([]db.ESDocument, int64, error) {
|
||||
return uc.repo.Data(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) DocumentGet(ctx context.Context, req *request.DatabaseESDocumentGet) (*db.ESDocument, error) {
|
||||
return uc.repo.DocumentGet(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) DocumentSet(ctx context.Context, req *request.DatabaseESDocumentSet) error {
|
||||
return uc.repo.DocumentSet(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) DocumentDelete(ctx context.Context, req *request.DatabaseESDocumentDelete) error {
|
||||
return uc.repo.DocumentDelete(ctx, req)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,60 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"context"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
type DatabaseRedisRepo interface {
|
||||
Databases(req *request.DatabaseRedisDatabases) (int, error)
|
||||
Data(req *request.DatabaseRedisData) ([]db.RedisKV, int, error)
|
||||
KeyGet(req *request.DatabaseRedisKeyGet) (*db.RedisKV, error)
|
||||
KeySet(req *request.DatabaseRedisKeySet) error
|
||||
KeyDelete(req *request.DatabaseRedisKeyDelete) error
|
||||
KeyTTL(req *request.DatabaseRedisKeyTTL) error
|
||||
KeyRename(req *request.DatabaseRedisKeyRename) error
|
||||
Clear(req *request.DatabaseRedisClear) error
|
||||
Databases(ctx context.Context, req *request.DatabaseRedisDatabases) (int, error)
|
||||
Data(ctx context.Context, req *request.DatabaseRedisData) ([]db.RedisKV, int, error)
|
||||
KeyGet(ctx context.Context, req *request.DatabaseRedisKeyGet) (*db.RedisKV, error)
|
||||
KeySet(ctx context.Context, req *request.DatabaseRedisKeySet) error
|
||||
KeyDelete(ctx context.Context, req *request.DatabaseRedisKeyDelete) error
|
||||
KeyTTL(ctx context.Context, req *request.DatabaseRedisKeyTTL) error
|
||||
KeyRename(ctx context.Context, req *request.DatabaseRedisKeyRename) error
|
||||
Clear(ctx context.Context, req *request.DatabaseRedisClear) error
|
||||
}
|
||||
|
||||
// DatabaseRedisUsecase Redis 业务用例
|
||||
type DatabaseRedisUsecase struct {
|
||||
repo DatabaseRedisRepo
|
||||
}
|
||||
|
||||
func NewDatabaseRedisUsecase(repo DatabaseRedisRepo) *DatabaseRedisUsecase {
|
||||
return &DatabaseRedisUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) Databases(ctx context.Context, req *request.DatabaseRedisDatabases) (int, error) {
|
||||
return uc.repo.Databases(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) Data(ctx context.Context, req *request.DatabaseRedisData) ([]db.RedisKV, int, error) {
|
||||
return uc.repo.Data(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeyGet(ctx context.Context, req *request.DatabaseRedisKeyGet) (*db.RedisKV, error) {
|
||||
return uc.repo.KeyGet(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeySet(ctx context.Context, req *request.DatabaseRedisKeySet) error {
|
||||
return uc.repo.KeySet(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeyDelete(ctx context.Context, req *request.DatabaseRedisKeyDelete) error {
|
||||
return uc.repo.KeyDelete(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeyTTL(ctx context.Context, req *request.DatabaseRedisKeyTTL) error {
|
||||
return uc.repo.KeyTTL(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeyRename(ctx context.Context, req *request.DatabaseRedisKeyRename) error {
|
||||
return uc.repo.KeyRename(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) Clear(ctx context.Context, req *request.DatabaseRedisClear) error {
|
||||
return uc.repo.Clear(ctx, req)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
type DatabaseServerStatus string
|
||||
@@ -62,14 +69,194 @@ func (r *DatabaseServer) AfterFind(tx *gorm.DB) error {
|
||||
|
||||
type DatabaseServerRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint, typ string) ([]*DatabaseServer, int64, error)
|
||||
Get(id uint) (*DatabaseServer, error)
|
||||
GetByName(name string) (*DatabaseServer, error)
|
||||
Create(req *request.DatabaseServerCreate) error
|
||||
Update(req *request.DatabaseServerUpdate) error
|
||||
List(ctx context.Context, page, limit uint, typ string) ([]*DatabaseServer, int64, error)
|
||||
Get(ctx context.Context, id uint) (*DatabaseServer, error)
|
||||
GetByName(ctx context.Context, name string) (*DatabaseServer, error)
|
||||
Create(server *DatabaseServer) error
|
||||
Save(server *DatabaseServer) error
|
||||
UpdateRemark(req *request.DatabaseServerUpdateRemark) error
|
||||
UpdatePassword(name string, password string) error
|
||||
UpdatePort(name string, port uint) error
|
||||
Delete(id uint) error
|
||||
ClearUsers(id uint) error
|
||||
Sync(id uint) error
|
||||
ListUsers(serverID uint) ([]*DatabaseUser, error)
|
||||
CreateUser(user *DatabaseUser) error
|
||||
Operator(ctx context.Context, server *DatabaseServer) (db.Operator, error)
|
||||
CheckServer(ctx context.Context, server *DatabaseServer) bool
|
||||
}
|
||||
|
||||
// DatabaseServerUsecase 数据库服务器业务用例
|
||||
type DatabaseServerUsecase struct {
|
||||
repo DatabaseServerRepo
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseServerUsecase(t *gotext.Locale, log *slog.Logger, databaseServerRepo DatabaseServerRepo) (*DatabaseServerUsecase, error) {
|
||||
return &DatabaseServerUsecase{
|
||||
repo: databaseServerRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Count() (int64, error) {
|
||||
return uc.repo.Count()
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) List(ctx context.Context, page, limit uint, typ string) ([]*DatabaseServer, int64, error) {
|
||||
return uc.repo.List(ctx, page, limit, typ)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Get(ctx context.Context, id uint) (*DatabaseServer, error) {
|
||||
return uc.repo.Get(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) GetByName(ctx context.Context, name string) (*DatabaseServer, error) {
|
||||
return uc.repo.GetByName(ctx, name)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Create(ctx context.Context, req *request.DatabaseServerCreate) error {
|
||||
databaseServer := &DatabaseServer{
|
||||
Name: req.Name,
|
||||
Type: DatabaseType(req.Type),
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
if !uc.repo.CheckServer(ctx, databaseServer) {
|
||||
return errors.New(uc.t.Get("check server connection failed"))
|
||||
}
|
||||
|
||||
return uc.repo.Create(databaseServer)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Update(ctx context.Context, req *request.DatabaseServerUpdate) error {
|
||||
server, err := uc.repo.Get(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server.Name = req.Name
|
||||
server.Host = req.Host
|
||||
server.Port = req.Port
|
||||
server.Username = req.Username
|
||||
server.Password = req.Password
|
||||
server.Remark = req.Remark
|
||||
|
||||
if !uc.repo.CheckServer(ctx, server) {
|
||||
return errors.New(uc.t.Get("check server connection failed"))
|
||||
}
|
||||
|
||||
return uc.repo.Save(server)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) UpdateRemark(req *request.DatabaseServerUpdateRemark) error {
|
||||
return uc.repo.UpdateRemark(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) UpdatePassword(name string, password string) error {
|
||||
return uc.repo.UpdatePassword(name, password)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) UpdatePort(name string, port uint) error {
|
||||
return uc.repo.UpdatePort(name, port)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Delete(id uint) error {
|
||||
return uc.repo.Delete(id)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) ClearUsers(id uint) error {
|
||||
return uc.repo.ClearUsers(id)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Sync(ctx context.Context, id uint) error {
|
||||
server, err := uc.repo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 非 Operator 类型不支持用户同步
|
||||
switch server.Type {
|
||||
case DatabaseTypeRedis, DatabaseTypeMongoDB, DatabaseTypeSQLite, DatabaseTypeElasticsearch:
|
||||
return fmt.Errorf("sync is not supported for %s", server.Type)
|
||||
}
|
||||
|
||||
users, err := uc.repo.ListUsers(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(ctx, server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer operator.Close()
|
||||
|
||||
switch server.Type {
|
||||
case DatabaseTypeMysql:
|
||||
allUsers, err := operator.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for user := range slices.Values(allUsers) {
|
||||
if !slices.ContainsFunc(users, func(a *DatabaseUser) bool {
|
||||
return a.Username == user.User && a.Host == user.Host
|
||||
}) && !slices.Contains([]string{"root", "mysql.sys", "mysql.session", "mysql.infoschema"}, user.User) {
|
||||
newUser := &DatabaseUser{
|
||||
ServerID: id,
|
||||
Username: user.User,
|
||||
Host: user.Host,
|
||||
Remark: uc.t.Get("sync from server %s", server.Name),
|
||||
}
|
||||
if err = uc.repo.CreateUser(newUser); err != nil {
|
||||
uc.log.Warn("sync mysql database user failed", slog.String("type", OperationTypeDatabaseServer), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
case DatabaseTypePostgresql:
|
||||
allUsers, err := operator.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for user := range slices.Values(allUsers) {
|
||||
if !slices.ContainsFunc(users, func(a *DatabaseUser) bool {
|
||||
return a.Username == user.User
|
||||
}) && !slices.Contains([]string{"postgres"}, user.User) {
|
||||
newUser := &DatabaseUser{
|
||||
ServerID: id,
|
||||
Username: user.User,
|
||||
Remark: uc.t.Get("sync from server %s", server.Name),
|
||||
}
|
||||
if err = uc.repo.CreateUser(newUser); err != nil {
|
||||
uc.log.Warn("sync postgresql database user failed", slog.String("type", OperationTypeDatabaseServer), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
case DatabaseTypeClickHouse:
|
||||
allUsers, err := operator.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for user := range slices.Values(allUsers) {
|
||||
if !slices.ContainsFunc(users, func(a *DatabaseUser) bool {
|
||||
return a.Username == user.User
|
||||
}) && !slices.Contains([]string{"default"}, user.User) {
|
||||
newUser := &DatabaseUser{
|
||||
ServerID: id,
|
||||
Username: user.User,
|
||||
Remark: uc.t.Get("sync from server %s", server.Name),
|
||||
}
|
||||
if err = uc.repo.CreateUser(newUser); err != nil {
|
||||
uc.log.Warn("sync clickhouse database user failed", slog.String("type", OperationTypeDatabaseServer), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
type DatabaseUserStatus string
|
||||
@@ -64,11 +67,203 @@ func (r *DatabaseUser) AfterFind(tx *gorm.DB) error {
|
||||
|
||||
type DatabaseUserRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint, typ string) ([]*DatabaseUser, int64, error)
|
||||
Get(id uint) (*DatabaseUser, error)
|
||||
Create(ctx context.Context, req *request.DatabaseUserCreate) error
|
||||
Update(req *request.DatabaseUserUpdate) error
|
||||
UpdateRemark(req *request.DatabaseUserUpdateRemark) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
DeleteByNames(serverID uint, names []string) error
|
||||
List(ctx context.Context, page, limit uint, typ string) ([]*DatabaseUser, int64, error)
|
||||
Get(ctx context.Context, id uint) (*DatabaseUser, error)
|
||||
UpdateRemark(ctx context.Context, req *request.DatabaseUserUpdateRemark) error
|
||||
Operator(ctx context.Context, server *DatabaseServer) (db.Operator, error)
|
||||
Upsert(user *DatabaseUser) error
|
||||
Save(user *DatabaseUser) error
|
||||
DeleteByID(id uint) error
|
||||
ListByNames(serverID uint, names []string) ([]*DatabaseUser, error)
|
||||
DeleteByServerNames(serverID uint, names []string) error
|
||||
}
|
||||
|
||||
// DatabaseUserUsecase 数据库用户业务用例
|
||||
type DatabaseUserUsecase struct {
|
||||
repo DatabaseUserRepo
|
||||
server DatabaseServerRepo
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseUserUsecase(log *slog.Logger, databaseServerRepo DatabaseServerRepo, databaseUserRepo DatabaseUserRepo) (*DatabaseUserUsecase, error) {
|
||||
return &DatabaseUserUsecase{
|
||||
repo: databaseUserRepo,
|
||||
server: databaseServerRepo,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Count() (int64, error) {
|
||||
return uc.repo.Count()
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) List(ctx context.Context, page, limit uint, typ string) ([]*DatabaseUser, int64, error) {
|
||||
return uc.repo.List(ctx, page, limit, typ)
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Get(ctx context.Context, id uint) (*DatabaseUser, error) {
|
||||
return uc.repo.Get(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Create(ctx context.Context, req *request.DatabaseUserCreate) error {
|
||||
server, err := uc.server.Get(ctx, req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(ctx, server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer operator.Close()
|
||||
|
||||
// 创建用户
|
||||
if err = operator.UserCreate(req.Username, req.Password, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建数据库并授权
|
||||
for name := range slices.Values(req.Privileges) {
|
||||
if err = operator.DatabaseCreate(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = operator.PrivilegesGrant(req.Username, name, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
user := &DatabaseUser{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
Host: req.Host,
|
||||
Password: req.Password,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
if err = uc.repo.Upsert(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("database user created", slog.String("type", OperationTypeDatabaseUser), slog.Uint64("operator_id", operatorID(ctx)), slog.String("username", req.Username), slog.Uint64("server_id", uint64(req.ServerID)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Update(ctx context.Context, req *request.DatabaseUserUpdate) error {
|
||||
user, err := uc.repo.Get(ctx, req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := uc.server.Get(ctx, user.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(ctx, server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer operator.Close()
|
||||
|
||||
// 更新密码
|
||||
if req.Password != "" {
|
||||
if err = operator.UserPassword(user.Username, req.Password, user.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = req.Password
|
||||
}
|
||||
|
||||
// 撤销被移除的权限
|
||||
currentPrivileges, _ := operator.UserPrivileges(user.Username, user.Host)
|
||||
for name := range slices.Values(currentPrivileges) {
|
||||
if !slices.Contains(req.Privileges, name) {
|
||||
_ = operator.PrivilegesRevoke(user.Username, name, user.Host)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建数据库并授权
|
||||
for name := range slices.Values(req.Privileges) {
|
||||
if err = operator.DatabaseCreate(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = operator.PrivilegesGrant(user.Username, name, user.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
user.Remark = req.Remark
|
||||
|
||||
return uc.repo.Save(user)
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) UpdateRemark(ctx context.Context, req *request.DatabaseUserUpdateRemark) error {
|
||||
return uc.repo.UpdateRemark(ctx, req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Delete(ctx context.Context, id uint) error {
|
||||
user, err := uc.repo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := uc.server.Get(ctx, user.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(ctx, server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer operator.Close()
|
||||
|
||||
_ = operator.UserDrop(user.Username, user.Host)
|
||||
|
||||
if err = uc.repo.DeleteByID(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("database user deleted", slog.String("type", OperationTypeDatabaseUser), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("username", user.Username))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) DeleteByNames(ctx context.Context, serverID uint, names []string) error {
|
||||
server, err := uc.server.Get(ctx, serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(ctx, server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer operator.Close()
|
||||
|
||||
switch server.Type {
|
||||
case DatabaseTypeMysql:
|
||||
users, err := uc.repo.ListByNames(serverID, names)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for name := range slices.Values(names) {
|
||||
host := "localhost"
|
||||
for u := range slices.Values(users) {
|
||||
if u.Username == name {
|
||||
host = u.Host
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = operator.UserDrop(name, host)
|
||||
}
|
||||
case DatabaseTypePostgresql, DatabaseTypeClickHouse:
|
||||
for name := range slices.Values(names) {
|
||||
_ = operator.UserDrop(name)
|
||||
}
|
||||
}
|
||||
|
||||
return uc.repo.DeleteByServerNames(serverID, names)
|
||||
}
|
||||
|
||||
+154
-7
@@ -1,18 +1,165 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/pkg/api"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type EnvironmentRepo interface {
|
||||
Types() []types.LV
|
||||
All(typ ...string) api.Environments
|
||||
IsInstalled(typ, slug string) bool
|
||||
InstalledSlugs(typ string) []string
|
||||
InstalledVersion(typ, slug string) string
|
||||
HasUpdate(typ, slug string) bool
|
||||
Install(typ, slug string) error
|
||||
Uninstall(typ, slug string) error
|
||||
Update(typ, slug string) error
|
||||
ScriptCommand(typ, action, slug, version string) string
|
||||
ExecScript(cmd string) error
|
||||
}
|
||||
|
||||
type EnvironmentUsecase struct {
|
||||
repo EnvironmentRepo
|
||||
cache CacheRepo
|
||||
task TaskRepo
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewEnvironmentUsecase(t *gotext.Locale, cacheRepo CacheRepo, environmentRepo EnvironmentRepo, taskRepo TaskRepo) (*EnvironmentUsecase, error) {
|
||||
return &EnvironmentUsecase{
|
||||
repo: environmentRepo,
|
||||
cache: cacheRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) Types() []types.LV {
|
||||
return []types.LV{
|
||||
{Label: "Go", Value: "go"},
|
||||
{Label: "Java", Value: "java"},
|
||||
{Label: "Node.js", Value: "nodejs"},
|
||||
{Label: "PHP", Value: "php"},
|
||||
{Label: "Python", Value: "python"},
|
||||
{Label: ".NET", Value: "dotnet"},
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) All(typ ...string) api.Environments {
|
||||
cached, err := uc.cache.Get(CacheKeyEnvironment)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var environments api.Environments
|
||||
if err = json.Unmarshal([]byte(cached), &environments); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 过滤
|
||||
environments = slices.DeleteFunc(environments, func(env *api.Environment) bool {
|
||||
return len(typ) > 0 && typ[0] != "" && env.Type != typ[0]
|
||||
})
|
||||
|
||||
return environments
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) getByTypeAndSlug(typ, slug string) *api.Environment {
|
||||
all := uc.All()
|
||||
for _, env := range all {
|
||||
if env.Type == typ && env.Slug == slug {
|
||||
return env
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) IsInstalled(typ, slug string) bool {
|
||||
return uc.repo.IsInstalled(typ, slug)
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) InstalledSlugs(typ string) []string {
|
||||
var slugs []string
|
||||
all := uc.All()
|
||||
for _, env := range all {
|
||||
if env.Type != typ {
|
||||
continue
|
||||
}
|
||||
if uc.repo.IsInstalled(typ, env.Slug) {
|
||||
slugs = append(slugs, env.Slug)
|
||||
}
|
||||
}
|
||||
return slugs
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) InstalledVersion(typ, slug string) string {
|
||||
return uc.repo.InstalledVersion(typ, slug)
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) HasUpdate(typ, slug string) bool {
|
||||
if !uc.repo.IsInstalled(typ, slug) {
|
||||
return false
|
||||
}
|
||||
env := uc.getByTypeAndSlug(typ, slug)
|
||||
if env == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
mainlineVersion := env.Version
|
||||
installedVersion := uc.repo.InstalledVersion(typ, slug)
|
||||
|
||||
return mainlineVersion != installedVersion && mainlineVersion != "" && installedVersion != ""
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) Install(typ, slug string) error {
|
||||
if installed := uc.repo.IsInstalled(typ, slug); installed {
|
||||
return errors.New(uc.t.Get("environment %s-%s already installed", typ, slug))
|
||||
}
|
||||
return uc.do(typ, slug, "install")
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) Uninstall(typ, slug string) error {
|
||||
if installed := uc.repo.IsInstalled(typ, slug); !installed {
|
||||
return errors.New(uc.t.Get("environment %s-%s not installed", typ, slug))
|
||||
}
|
||||
return uc.do(typ, slug, "uninstall")
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) Update(typ, slug string) error {
|
||||
if installed := uc.repo.IsInstalled(typ, slug); !installed {
|
||||
return errors.New(uc.t.Get("environment %s-%s not installed", typ, slug))
|
||||
}
|
||||
return uc.do(typ, slug, "update")
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) do(typ, slug, action string) error {
|
||||
env := uc.getByTypeAndSlug(typ, slug)
|
||||
if env == nil {
|
||||
return fmt.Errorf("environment not found: %s-%s", typ, slug)
|
||||
}
|
||||
|
||||
cmd := uc.repo.ScriptCommand(typ, action, env.Slug, env.Version)
|
||||
|
||||
if app.IsCli {
|
||||
return uc.repo.ExecScript(cmd)
|
||||
}
|
||||
|
||||
var name string
|
||||
switch action {
|
||||
case "install":
|
||||
name = uc.t.Get("Install environment %s", env.Name)
|
||||
case "uninstall":
|
||||
name = uc.t.Get("Uninstall environment %s", env.Name)
|
||||
case "update":
|
||||
name = uc.t.Get("Update environment %s", env.Name)
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = fmt.Sprintf("environment:%s:%s", typ, slug)
|
||||
task.Name = name
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
)
|
||||
|
||||
type FileShare struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Token string `gorm:"not null;uniqueIndex" json:"token"` // 唯一标识(用于下载 URL)
|
||||
Path string `gorm:"not null;index" json:"path"` // 文件路径(同一文件可有多条分享)
|
||||
Downloads uint `gorm:"not null;default:0" json:"downloads"` // 已下载次数
|
||||
MaxDownloads uint `gorm:"not null;default:0" json:"max_downloads"` // 最大下载次数(0 不限)
|
||||
ExpiredAt time.Time `gorm:"not null;index" json:"expired_at"` // 过期时间
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FileShareRepo interface {
|
||||
List() ([]*FileShare, error)
|
||||
Get(id uint) (*FileShare, error)
|
||||
Create(path string, maxDownloads uint, expiredAt time.Time) (*FileShare, error)
|
||||
Delete(id uint) error
|
||||
Consume(token string, count bool) (*FileShare, error)
|
||||
ClearExpired() (int64, error)
|
||||
}
|
||||
|
||||
type FileShareUsecase struct {
|
||||
repo FileShareRepo
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewFileShareUsecase(log *slog.Logger, fileShareRepo FileShareRepo) (*FileShareUsecase, error) {
|
||||
return &FileShareUsecase{
|
||||
repo: fileShareRepo,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *FileShareUsecase) List() ([]*FileShare, error) {
|
||||
return uc.repo.List()
|
||||
}
|
||||
|
||||
// Create 创建分享,同一路径可重复分享为多条独立记录
|
||||
func (uc *FileShareUsecase) Create(ctx context.Context, req *request.FileShareCreate) (*FileShare, error) {
|
||||
share, err := uc.repo.Create(req.Path, req.MaxDownloads, time.Now().Add(time.Duration(req.ExpireHours)*time.Hour))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("file share created", slog.String("type", OperationTypeFile), slog.Uint64("operator_id", operatorID(ctx)), slog.String("path", req.Path))
|
||||
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (uc *FileShareUsecase) Delete(ctx context.Context, id uint) error {
|
||||
share, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("file share deleted", slog.String("type", OperationTypeFile), slog.Uint64("operator_id", operatorID(ctx)), slog.String("path", share.Path))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Consume 校验分享有效性,count 为真时原子计数一次下载
|
||||
func (uc *FileShareUsecase) Consume(token string, count bool) (*FileShare, error) {
|
||||
return uc.repo.Consume(token, count)
|
||||
}
|
||||
|
||||
func (uc *FileShareUsecase) ClearExpired() (int64, error) {
|
||||
return uc.repo.ClearExpired()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// operatorID 从 context 获取操作员 ID,无法获取时返回 0(系统操作)
|
||||
func operatorID(ctx context.Context) uint64 {
|
||||
if ctx == nil {
|
||||
return 0
|
||||
}
|
||||
userID := ctx.Value("user_id")
|
||||
if userID == nil {
|
||||
return 0
|
||||
}
|
||||
return cast.ToUint64(userID)
|
||||
}
|
||||
|
||||
// IsNotFound 判断错误是否为记录不存在
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
|
||||
// containerSock 从设置读取容器 socket 路径,未配置或失败时返回默认值
|
||||
func containerSock(setting SettingRepo) string {
|
||||
sock, _ := setting.Get(SettingKeyContainerSock)
|
||||
if sock == "" {
|
||||
sock = "/var/run/docker.sock"
|
||||
}
|
||||
// 自动补全 scheme
|
||||
if !strings.Contains(sock, "://") {
|
||||
sock = fmt.Sprintf("unix://%s", sock)
|
||||
}
|
||||
return sock
|
||||
}
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
OperationTypeMonitor = "monitor"
|
||||
OperationTypeWebhook = "webhook"
|
||||
OperationTypeUser = "user"
|
||||
OperationTypeWaf = "waf"
|
||||
)
|
||||
|
||||
// LogEntry 日志条目
|
||||
@@ -52,3 +53,19 @@ type LogRepo interface {
|
||||
// ListDates 获取可用的日志日期列表
|
||||
ListDates(logType string) ([]string, error)
|
||||
}
|
||||
|
||||
type LogUsecase struct {
|
||||
repo LogRepo
|
||||
}
|
||||
|
||||
func NewLogUsecase(repo LogRepo) *LogUsecase {
|
||||
return &LogUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *LogUsecase) List(logType string, limit int, date string) ([]LogEntry, error) {
|
||||
return uc.repo.List(logType, limit, date)
|
||||
}
|
||||
|
||||
func (uc *LogUsecase) ListDates(logType string) ([]string, error) {
|
||||
return uc.repo.ListDates(logType)
|
||||
}
|
||||
|
||||
+61
-3
@@ -3,7 +3,9 @@ package biz
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -15,8 +17,64 @@ type Monitor struct {
|
||||
}
|
||||
|
||||
type MonitorRepo interface {
|
||||
GetSetting() (*request.MonitorSetting, error)
|
||||
UpdateSetting(setting *request.MonitorSetting) error
|
||||
Clear() error
|
||||
List(start, end time.Time) ([]*Monitor, error)
|
||||
}
|
||||
|
||||
type MonitorUsecase struct {
|
||||
repo MonitorRepo
|
||||
setting SettingRepo
|
||||
}
|
||||
|
||||
func NewMonitorUsecase(repo MonitorRepo, setting SettingRepo) *MonitorUsecase {
|
||||
return &MonitorUsecase{repo: repo, setting: setting}
|
||||
}
|
||||
|
||||
func (uc *MonitorUsecase) GetSetting() (*request.MonitorSetting, error) {
|
||||
monitor, err := uc.setting.Get(SettingKeyMonitor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
monitorDays, err := uc.setting.Get(SettingKeyMonitorDays)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
monitorInterval, err := uc.setting.GetInt(SettingKeyMonitorInterval, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
alertDays, err := uc.setting.GetInt(SettingKeyAlertLogDays, 30)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setting := new(request.MonitorSetting)
|
||||
setting.Enabled = cast.ToBool(monitor)
|
||||
setting.Days = cast.ToUint(monitorDays)
|
||||
setting.Interval = uint(monitorInterval)
|
||||
setting.AlertDays = uint(alertDays)
|
||||
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
func (uc *MonitorUsecase) UpdateSetting(setting *request.MonitorSetting) error {
|
||||
if err := uc.setting.Set(SettingKeyMonitor, cast.ToString(setting.Enabled)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := uc.setting.Set(SettingKeyMonitorDays, cast.ToString(setting.Days)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := uc.setting.Set(SettingKeyMonitorInterval, cast.ToString(setting.Interval)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.setting.Set(SettingKeyAlertLogDays, cast.ToString(setting.AlertDays))
|
||||
}
|
||||
|
||||
func (uc *MonitorUsecase) Clear() error {
|
||||
return uc.repo.Clear()
|
||||
}
|
||||
|
||||
func (uc *MonitorUsecase) List(start, end time.Time) ([]*Monitor, error) {
|
||||
return uc.repo.List(start, end)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/notify"
|
||||
)
|
||||
|
||||
// NotifyChannel 通知渠道
|
||||
type NotifyChannel struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;default:''" json:"name"`
|
||||
Type string `gorm:"not null;default:''" json:"type"` // smtp
|
||||
Config json.RawMessage `gorm:"not null;default:''" json:"config"` // 渠道配置,含凭据,落库前整体加密
|
||||
Enabled bool `gorm:"not null;default:true" json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (r *NotifyChannel) BeforeSave(tx *gorm.DB) error {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encrypted, err := crypter.Encrypt(r.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Config = json.RawMessage(encrypted)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NotifyChannel) AfterFind(tx *gorm.DB) error {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if config, err := crypter.Decrypt(string(r.Config)); err == nil {
|
||||
r.Config = config
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NotifyEvent 系统事件类型
|
||||
type NotifyEvent string
|
||||
|
||||
const (
|
||||
NotifyEventCertRenew NotifyEvent = "cert_renew" // 证书续签失败
|
||||
NotifyEventBackup NotifyEvent = "backup" // 备份失败
|
||||
NotifyEventTaskFailed NotifyEvent = "task_failed" // 后台任务失败
|
||||
NotifyEventCronFailed NotifyEvent = "cron_failed" // 计划任务执行失败
|
||||
NotifyEventWebsiteExpire NotifyEvent = "website_expire" // 网站到期关停
|
||||
NotifyEventTamper NotifyEvent = "tamper" // 防篡改拦截
|
||||
NotifyEventHealth NotifyEvent = "health" // 面板健康问题
|
||||
NotifyEventLogin NotifyEvent = "login" // 面板登录
|
||||
NotifyEventLoginFailed NotifyEvent = "login_failed" // 面板登录失败过多
|
||||
NotifyEventSSHLogin NotifyEvent = "ssh_login" // SSH 登录
|
||||
NotifyEventSSHBruteforce NotifyEvent = "ssh_bruteforce" // SSH 爆破
|
||||
)
|
||||
|
||||
type NotifyChannelRepo interface {
|
||||
List(page, limit uint) ([]*NotifyChannel, int64, error)
|
||||
All() ([]*NotifyChannel, error)
|
||||
Get(id uint) (*NotifyChannel, error)
|
||||
GetByIDs(ids []uint) ([]*NotifyChannel, error)
|
||||
Create(channel *NotifyChannel) error
|
||||
Update(channel *NotifyChannel) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
// notifyMaxPending 异步事件通知的并发上限,防止高频事件堆积 goroutine
|
||||
const notifyMaxPending = 32
|
||||
|
||||
type NotifyUsecase struct {
|
||||
repo NotifyChannelRepo
|
||||
setting SettingRepo
|
||||
log *slog.Logger
|
||||
t *gotext.Locale
|
||||
pending chan struct{}
|
||||
}
|
||||
|
||||
func NewNotifyUsecase(t *gotext.Locale, log *slog.Logger, notifyChannelRepo NotifyChannelRepo, settingRepo SettingRepo) (*NotifyUsecase, error) {
|
||||
return &NotifyUsecase{
|
||||
repo: notifyChannelRepo,
|
||||
setting: settingRepo,
|
||||
log: log,
|
||||
t: t,
|
||||
pending: make(chan struct{}, notifyMaxPending),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) List(page, limit uint) ([]*NotifyChannel, int64, error) {
|
||||
return uc.repo.List(page, limit)
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) All() ([]*NotifyChannel, error) {
|
||||
return uc.repo.All()
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) Get(id uint) (*NotifyChannel, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) Create(ctx context.Context, req *request.NotifyChannelCreate) (*NotifyChannel, error) {
|
||||
// 提前构造一次,配置不合法直接拒绝入库
|
||||
if _, err := notify.New(req.Type, req.Config); err != nil {
|
||||
return nil, errors.New(uc.t.Get("invalid channel config: %v", err))
|
||||
}
|
||||
|
||||
channel := &NotifyChannel{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Config: req.Config,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
if err := uc.repo.Create(channel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.log.Info("notify channel created", slog.String("type", OperationTypeSetting), slog.Uint64("operator_id", operatorID(ctx)), slog.String("name", req.Name))
|
||||
|
||||
// 落库时 Config 已被加密,重新读取以返回解密后的实体
|
||||
return uc.repo.Get(channel.ID)
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) Update(ctx context.Context, req *request.NotifyChannelUpdate) error {
|
||||
channel, err := uc.repo.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = notify.New(req.Type, req.Config); err != nil {
|
||||
return errors.New(uc.t.Get("invalid channel config: %v", err))
|
||||
}
|
||||
|
||||
channel.Name = req.Name
|
||||
channel.Type = req.Type
|
||||
channel.Config = req.Config
|
||||
channel.Enabled = req.Enabled
|
||||
if err = uc.repo.Update(channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.log.Info("notify channel updated", slog.String("type", OperationTypeSetting), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", req.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) Delete(ctx context.Context, id uint) error {
|
||||
channel, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.log.Info("notify channel deleted", slog.String("type", OperationTypeSetting), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", channel.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test 向指定渠道发送一条测试消息
|
||||
func (uc *NotifyUsecase) Test(ctx context.Context, id uint) error {
|
||||
channel, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.dispatch(ctx, channel, uc.t.Get("[AcePanel] Test Notification"),
|
||||
NotifyBody(uc.t.Get("This is a test notification from AcePanel, receiving it means the channel is configured correctly."), [][2]string{
|
||||
{uc.t.Get("Channel"), channel.Name},
|
||||
{uc.t.Get("Time"), time.Now().Format(time.DateTime)},
|
||||
}))
|
||||
}
|
||||
|
||||
// Send 向指定渠道列表发送通知,返回成功发送的渠道数,部分渠道失败不影响其他渠道
|
||||
func (uc *NotifyUsecase) Send(ctx context.Context, channelIDs []uint, subject, body string) (int, error) {
|
||||
if len(channelIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
channels, err := uc.repo.GetByIDs(channelIDs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var sent int
|
||||
var errs []error
|
||||
for _, channel := range channels {
|
||||
if !channel.Enabled {
|
||||
continue
|
||||
}
|
||||
if err = uc.dispatch(ctx, channel, subject, body); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", channel.Name, err))
|
||||
continue
|
||||
}
|
||||
sent++
|
||||
}
|
||||
|
||||
return sent, errors.Join(errs...)
|
||||
}
|
||||
|
||||
// SendEvent 发送系统事件通知,不阻塞业务流程
|
||||
// 待发送数超过上限时丢弃并告知,避免慢渠道拖垮调用方
|
||||
func (uc *NotifyUsecase) SendEvent(event NotifyEvent, subject, body string) {
|
||||
select {
|
||||
case uc.pending <- struct{}{}:
|
||||
default:
|
||||
uc.log.Warn("event notification dropped, too many pending sends", slog.String("event", string(event)))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() { <-uc.pending }()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
if err := uc.SendEventSync(ctx, event, subject, body); err != nil {
|
||||
uc.log.Warn("failed to send event notification", slog.String("event", string(event)), slog.Any("err", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// SendEventSync 同步发送系统事件通知,未订阅该事件或未配置渠道时静默跳过
|
||||
// 供 CLI 等短生命周期进程使用,异步发送会随进程退出丢失
|
||||
func (uc *NotifyUsecase) SendEventSync(ctx context.Context, event NotifyEvent, subject, body string) error {
|
||||
setting, err := uc.GetSetting()
|
||||
if err != nil || len(setting.Channels) == 0 || !slices.Contains(setting.Events, string(event)) {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = uc.Send(ctx, setting.Channels, subject, body)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) GetSetting() (*request.NotifySetting, error) {
|
||||
events, err := uc.setting.GetSlice(SettingKeyNotifyEvents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channelsStr, err := uc.setting.Get(SettingKeyNotifyEventChannels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channels := make([]uint, 0)
|
||||
if channelsStr != "" {
|
||||
_ = json.Unmarshal([]byte(channelsStr), &channels)
|
||||
}
|
||||
|
||||
return &request.NotifySetting{
|
||||
Events: events,
|
||||
Channels: channels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) UpdateSetting(setting *request.NotifySetting) error {
|
||||
if err := uc.setting.SetSlice(SettingKeyNotifyEvents, setting.Events); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channels, err := json.Marshal(setting.Channels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.setting.Set(SettingKeyNotifyEventChannels, string(channels))
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) dispatch(ctx context.Context, channel *NotifyChannel, subject, body string) error {
|
||||
notifier, err := notify.New(channel.Type, channel.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return notifier.Send(ctx, ¬ify.Message{Subject: subject, Body: body})
|
||||
}
|
||||
|
||||
// NotifyBody 构建通知正文,rows 为「名称,值」明细
|
||||
func NotifyBody(summary string, rows [][2]string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<p>`)
|
||||
sb.WriteString(html.EscapeString(summary))
|
||||
sb.WriteString(`</p>`)
|
||||
|
||||
if len(rows) > 0 {
|
||||
sb.WriteString(`<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;border:1px solid #ddd">`)
|
||||
for _, row := range rows {
|
||||
sb.WriteString(`<tr><td style="border:1px solid #ddd;background:#fafafa">`)
|
||||
sb.WriteString(html.EscapeString(row[0]))
|
||||
sb.WriteString(`</td><td style="border:1px solid #ddd">`)
|
||||
sb.WriteString(html.EscapeString(row[1]))
|
||||
sb.WriteString(`</td></tr>`)
|
||||
}
|
||||
sb.WriteString(`</table>`)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
+139
-6
@@ -2,9 +2,16 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -19,9 +26,135 @@ type Project struct {
|
||||
|
||||
type ProjectRepo interface {
|
||||
Count() (int64, error)
|
||||
List(typ types.ProjectType, page, limit uint) ([]*types.ProjectDetail, int64, error)
|
||||
Get(id uint) (*types.ProjectDetail, error)
|
||||
Create(ctx context.Context, req *request.ProjectCreate) (*types.ProjectDetail, error)
|
||||
Update(ctx context.Context, req *request.ProjectUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
List(typ types.ProjectType, page, limit uint) ([]*Project, int64, error)
|
||||
GetEntity(id uint) (*Project, error)
|
||||
ParseDetail(project *Project) (*types.ProjectDetail, error)
|
||||
NameExists(name string) (bool, error)
|
||||
Create(project *Project, req *request.ProjectCreate) error
|
||||
Save(project *Project) error
|
||||
Delete(project *Project) error
|
||||
RenameUnitFile(old, new string) error
|
||||
RemoveUnitFile(name string) error
|
||||
UpdateUnitFile(name string, req *request.ProjectUpdate) error
|
||||
}
|
||||
|
||||
type ProjectUsecase struct {
|
||||
repo ProjectRepo
|
||||
log *slog.Logger
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewProjectUsecase(t *gotext.Locale, log *slog.Logger, projectRepo ProjectRepo) (*ProjectUsecase, error) {
|
||||
return &ProjectUsecase{
|
||||
repo: projectRepo,
|
||||
log: log,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *ProjectUsecase) Count() (int64, error) {
|
||||
return uc.repo.Count()
|
||||
}
|
||||
|
||||
func (uc *ProjectUsecase) List(typ types.ProjectType, page, limit uint) ([]*types.ProjectDetail, int64, error) {
|
||||
projects, total, err := uc.repo.List(typ, page, limit)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
details := lo.Map(projects, func(p *Project, _ int) *types.ProjectDetail {
|
||||
detail, err := uc.repo.ParseDetail(p)
|
||||
if err != nil {
|
||||
// 如果解析失败,返回基本信息
|
||||
return &types.ProjectDetail{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
}
|
||||
}
|
||||
return detail
|
||||
})
|
||||
|
||||
return details, total, nil
|
||||
}
|
||||
|
||||
func (uc *ProjectUsecase) Get(id uint) (*types.ProjectDetail, error) {
|
||||
project, err := uc.repo.GetEntity(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uc.repo.ParseDetail(project)
|
||||
}
|
||||
|
||||
func (uc *ProjectUsecase) Create(ctx context.Context, req *request.ProjectCreate) (*types.ProjectDetail, error) {
|
||||
// 检查项目名是否已存在
|
||||
exists, err := uc.repo.NameExists(req.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, errors.New(uc.t.Get("project name already exists"))
|
||||
}
|
||||
|
||||
project := &Project{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Path: lo.If(!strings.HasPrefix(req.RootDir, "/"), filepath.Join("/", req.RootDir)).Else(req.RootDir),
|
||||
}
|
||||
|
||||
if err := uc.repo.Create(project, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("project created", slog.String("type", OperationTypeProject), slog.Uint64("operator_id", operatorID(ctx)), slog.String("name", req.Name), slog.String("project_type", string(req.Type)))
|
||||
|
||||
return uc.repo.ParseDetail(project)
|
||||
}
|
||||
|
||||
func (uc *ProjectUsecase) Update(ctx context.Context, req *request.ProjectUpdate) error {
|
||||
project, err := uc.repo.GetEntity(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果名称变更,需要重命名 unit 文件
|
||||
if req.Name != project.Name {
|
||||
if err := uc.repo.RenameUnitFile(project.Name, req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
project.Name = req.Name
|
||||
}
|
||||
|
||||
project.Path = lo.If(!strings.HasPrefix(req.RootDir, "/"), filepath.Join("/", req.RootDir)).Else(req.RootDir)
|
||||
if err := uc.repo.Save(project); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("project updated", slog.String("type", OperationTypeProject), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", project.Name))
|
||||
|
||||
// 更新 systemd unit 文件
|
||||
return uc.repo.UpdateUnitFile(project.Name, req)
|
||||
}
|
||||
|
||||
func (uc *ProjectUsecase) Delete(ctx context.Context, id uint) error {
|
||||
project, err := uc.repo.GetEntity(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除 systemd unit 文件
|
||||
if err := uc.repo.RemoveUnitFile(project.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := uc.repo.Delete(project); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("project deleted", slog.String("type", OperationTypeProject), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", project.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+39
-2
@@ -1,8 +1,45 @@
|
||||
package biz
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type SafeRepo interface {
|
||||
GetPingStatus() (bool, error)
|
||||
UpdatePingStatus(ctx context.Context, status bool) error
|
||||
FirewallRunning() (bool, error)
|
||||
SetPingStatus(status bool) error
|
||||
}
|
||||
|
||||
type SafeUsecase struct {
|
||||
repo SafeRepo
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewSafeUsecase(repo SafeRepo, log *slog.Logger) *SafeUsecase {
|
||||
return &SafeUsecase{repo: repo, log: log}
|
||||
}
|
||||
|
||||
func (uc *SafeUsecase) GetPingStatus() (bool, error) {
|
||||
return uc.repo.GetPingStatus()
|
||||
}
|
||||
|
||||
func (uc *SafeUsecase) UpdatePingStatus(ctx context.Context, status bool) error {
|
||||
running, err := uc.repo.FirewallRunning()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !running {
|
||||
return fmt.Errorf("failed to update ping status: firewall is not running")
|
||||
}
|
||||
|
||||
if err = uc.repo.SetPingStatus(status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("ping status updated", slog.String("type", OperationTypeSafe), slog.Uint64("operator_id", operatorID(ctx)), slog.Bool("status", status))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+138
-3
@@ -1,6 +1,11 @@
|
||||
package biz
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// ScanEvent 扫描事件模型
|
||||
type ScanEvent struct {
|
||||
@@ -75,8 +80,138 @@ type ScanEventRepo interface {
|
||||
TopSourceIPs(start, end string, limit uint) ([]*ScanSourceRank, error)
|
||||
TopPorts(start, end string, limit uint) ([]*ScanPortRank, error)
|
||||
ClearBefore(date string) error
|
||||
GetSetting() (*ScanSetting, error)
|
||||
UpdateSetting(setting *ScanSetting) error
|
||||
Clear() error
|
||||
VacuumDB() error
|
||||
}
|
||||
|
||||
// ScanEventUsecase 扫描事件业务逻辑
|
||||
type ScanEventUsecase struct {
|
||||
repo ScanEventRepo
|
||||
setting SettingRepo
|
||||
}
|
||||
|
||||
func NewScanEventUsecase(repo ScanEventRepo, setting SettingRepo) *ScanEventUsecase {
|
||||
return &ScanEventUsecase{repo: repo, setting: setting}
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) Upsert(events []*ScanEvent) error {
|
||||
return uc.repo.Upsert(events)
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) List(start, end, sourceIP string, port uint, location string, page, limit uint) ([]*ScanEvent, uint, error) {
|
||||
return uc.repo.List(start, end, sourceIP, port, location, page, limit)
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) Summary(start, end string) (*ScanSummary, error) {
|
||||
return uc.repo.Summary(start, end)
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) Trend(start, end string) ([]*ScanDayTrend, error) {
|
||||
return uc.repo.Trend(start, end)
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) TopSourceIPs(start, end string, limit uint) ([]*ScanSourceRank, error) {
|
||||
return uc.repo.TopSourceIPs(start, end, limit)
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) TopPorts(start, end string, limit uint) ([]*ScanPortRank, error) {
|
||||
return uc.repo.TopPorts(start, end, limit)
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) ClearBefore(date string) error {
|
||||
return uc.repo.ClearBefore(date)
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) GetSetting() (*ScanSetting, error) {
|
||||
enabled, err := uc.setting.GetBool(SettingKeyScanAware)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
days, err := uc.setting.GetInt(SettingKeyScanAwareDays, 30)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
interfacesStr, err := uc.setting.Get(SettingKeyScanAwareInterfaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var interfaces []string
|
||||
if interfacesStr != "" {
|
||||
_ = json.Unmarshal([]byte(interfacesStr), &interfaces)
|
||||
}
|
||||
|
||||
autoBlock, err := uc.setting.GetBool(SettingKeyScanAwareAutoBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockThreshold, err := uc.setting.GetInt(SettingKeyScanAwareBlockThreshold, 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockWindow, err := uc.setting.GetInt(SettingKeyScanAwareBlockWindow, 5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockDuration, err := uc.setting.GetInt(SettingKeyScanAwareBlockDuration, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
whitelist, err := uc.setting.GetSlice(SettingKeyScanAwareWhitelist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ScanSetting{
|
||||
Enabled: enabled,
|
||||
Days: uint(days),
|
||||
Interfaces: interfaces,
|
||||
AutoBlock: autoBlock,
|
||||
BlockThreshold: uint(blockThreshold),
|
||||
BlockWindow: uint(blockWindow),
|
||||
BlockDuration: uint(blockDuration),
|
||||
Whitelist: whitelist,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) UpdateSetting(setting *ScanSetting) error {
|
||||
if err := uc.setting.Set(SettingKeyScanAware, cast.ToString(setting.Enabled)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := uc.setting.Set(SettingKeyScanAwareDays, cast.ToString(setting.Days)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interfacesJSON, err := json.Marshal(setting.Interfaces)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = uc.setting.Set(SettingKeyScanAwareInterfaces, string(interfacesJSON)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.setting.Set(SettingKeyScanAwareAutoBlock, cast.ToString(setting.AutoBlock)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = uc.setting.Set(SettingKeyScanAwareBlockThreshold, cast.ToString(setting.BlockThreshold)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = uc.setting.Set(SettingKeyScanAwareBlockWindow, cast.ToString(setting.BlockWindow)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = uc.setting.Set(SettingKeyScanAwareBlockDuration, cast.ToString(setting.BlockDuration)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.setting.SetSlice(SettingKeyScanAwareWhitelist, setting.Whitelist)
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) Clear() error {
|
||||
return uc.repo.Clear()
|
||||
}
|
||||
|
||||
func (uc *ScanEventUsecase) VacuumDB() error {
|
||||
return uc.repo.VacuumDB()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user