mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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,9 +12,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,9 +28,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 +42,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
|
||||
|
||||
+21
-7
@@ -1,25 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/injector"
|
||||
)
|
||||
|
||||
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()
|
||||
inj := injector.New()
|
||||
defer func() { _ = inj.Shutdown() }()
|
||||
|
||||
ace, err := do.Invoke[*app.Ace](inj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = ace.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ace.Run()
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
//go:build wireinject
|
||||
|
||||
package main
|
||||
|
||||
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/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))
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
|
||||
//go:build !wireinject
|
||||
// +build !wireinject
|
||||
|
||||
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/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"
|
||||
"github.com/acepanel/panel/v3/pkg/websitestat"
|
||||
)
|
||||
|
||||
import (
|
||||
_ "time/tzdata"
|
||||
)
|
||||
|
||||
// Injectors from wire.go:
|
||||
|
||||
// initAce init application.
|
||||
func initAce() (*app.Ace, error) {
|
||||
config, err := bootstrap.NewConf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := bootstrap.NewDB(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manager, err := bootstrap.NewSession(config, db)
|
||||
if err != nil {
|
||||
return 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)
|
||||
userTokenRepo := data.NewUserTokenRepo(locale, config, db)
|
||||
middlewares := middleware.NewMiddlewares(config, manager, appRepo, userTokenRepo)
|
||||
userRepo := data.NewUserRepo(locale, db, logger)
|
||||
userService := service.NewUserService(locale, config, manager, userRepo)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
userPasskeyService := service.NewUserPasskeyService(locale, config, manager, userPasskeyRepo, userRepo)
|
||||
userTokenService := service.NewUserTokenService(locale, userTokenRepo)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, logger, databaseServerRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(locale, db, logger, databaseServerRepo, databaseUserRepo)
|
||||
settingRepo := data.NewSettingRepo(locale, db, logger, config, taskRepo)
|
||||
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)
|
||||
projectRepo := data.NewProjectRepo(locale, db, logger)
|
||||
environmentRepo := data.NewEnvironmentRepo(locale, config, cacheRepo, taskRepo)
|
||||
cronRepo := data.NewCronRepo(locale, db, logger)
|
||||
backupRepo := data.NewBackupRepo(locale, config, db, logger, settingRepo, websiteRepo)
|
||||
containerRepo := data.NewContainerRepo(settingRepo)
|
||||
homeService := service.NewHomeService(locale, config, taskRepo, websiteRepo, projectRepo, appRepo, environmentRepo, settingRepo, cronRepo, backupRepo, containerRepo)
|
||||
taskService := service.NewTaskService(taskRepo)
|
||||
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
|
||||
projectService := service.NewProjectService(projectRepo, settingRepo)
|
||||
databaseService := service.NewDatabaseService(databaseRepo)
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
|
||||
databaseRedisRepo := data.NewDatabaseRedisRepo(locale, db, logger)
|
||||
databaseRedisService := service.NewDatabaseRedisService(databaseRedisRepo)
|
||||
databaseElasticsearchRepo := data.NewDatabaseElasticsearchRepo(locale, db, logger)
|
||||
databaseElasticsearchService := service.NewDatabaseElasticsearchService(databaseElasticsearchRepo)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
|
||||
backupService := service.NewBackupService(locale, backupRepo)
|
||||
backupAccountRepo := data.NewBackupAccountRepo(locale, db, logger, settingRepo)
|
||||
backupStorageService := service.NewBackupStorageService(locale, backupAccountRepo)
|
||||
certService := service.NewCertService(locale, certRepo)
|
||||
certDNSRepo := data.NewCertDNSRepo(db, logger)
|
||||
certDNSService := service.NewCertDNSService(certDNSRepo)
|
||||
certAccountService := service.NewCertAccountService(certAccountRepo)
|
||||
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)
|
||||
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo, loader)
|
||||
environmentService := service.NewEnvironmentService(locale, environmentRepo, taskRepo)
|
||||
environmentGoService := service.NewEnvironmentGoService(locale, environmentRepo)
|
||||
environmentJavaService := service.NewEnvironmentJavaService(locale, environmentRepo)
|
||||
environmentNodejsService := service.NewEnvironmentNodejsService(locale, environmentRepo)
|
||||
environmentPHPService := service.NewEnvironmentPHPService(locale, config, environmentRepo, taskRepo)
|
||||
environmentPythonService := service.NewEnvironmentPythonService(locale, environmentRepo)
|
||||
environmentDotnetService := service.NewEnvironmentDotnetService(locale, environmentRepo)
|
||||
cronService := service.NewCronService(cronRepo)
|
||||
processService := service.NewProcessService()
|
||||
safeRepo := data.NewSafeRepo(logger)
|
||||
safeService := service.NewSafeService(safeRepo)
|
||||
firewallService := service.NewFirewallService()
|
||||
scanEventRepo, err := data.NewScanEventRepo(settingRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
firewallScanService := service.NewFirewallScanService(scanEventRepo)
|
||||
sshRepo := data.NewSSHRepo(locale, db, logger)
|
||||
sshService := service.NewSSHService(sshRepo)
|
||||
containerService := service.NewContainerService(containerRepo)
|
||||
containerComposeRepo := data.NewContainerComposeRepo()
|
||||
containerComposeService := service.NewContainerComposeService(containerComposeRepo)
|
||||
containerNetworkRepo := data.NewContainerNetworkRepo(settingRepo)
|
||||
containerNetworkService := service.NewContainerNetworkService(containerNetworkRepo)
|
||||
containerImageRepo := data.NewContainerImageRepo(settingRepo)
|
||||
containerImageService := service.NewContainerImageService(containerImageRepo)
|
||||
containerVolumeRepo := data.NewContainerVolumeRepo(settingRepo)
|
||||
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
|
||||
fileService := service.NewFileService(locale, taskRepo)
|
||||
logRepo := data.NewLogRepo(db)
|
||||
logService := service.NewLogService(locale, logRepo)
|
||||
monitorRepo := data.NewMonitorRepo(db, settingRepo)
|
||||
monitorService := service.NewMonitorService(settingRepo, monitorRepo)
|
||||
settingService := service.NewSettingService(locale, db, settingRepo, certRepo, certAccountRepo)
|
||||
systemctlService := service.NewSystemctlService(locale)
|
||||
toolboxNetworkService := service.NewToolboxNetworkService()
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
toolboxSSHService := service.NewToolboxSSHService(locale)
|
||||
toolboxDiskService := service.NewToolboxDiskService(locale)
|
||||
toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo)
|
||||
toolboxMigrationService := service.NewToolboxMigrationService(locale, config, logger, settingRepo, websiteRepo, databaseRepo, databaseServerRepo, databaseUserRepo, projectRepo, appRepo, environmentRepo)
|
||||
webHookRepo := data.NewWebHookRepo(locale, db, logger)
|
||||
webHookService := service.NewWebHookService(webHookRepo)
|
||||
templateRepo := data.NewTemplateRepo(locale, logger, cacheRepo)
|
||||
templateService := service.NewTemplateService(locale, templateRepo, settingRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregator := websitestat.NewAggregator()
|
||||
websiteStatService := service.NewWebsiteStatService(settingRepo, websiteStatRepo, websiteRepo, aggregator)
|
||||
http := route.NewHttp(config, userService, userPasskeyService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseRedisService, databaseElasticsearchService, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, environmentDotnetService, cronService, processService, safeService, firewallService, firewallScanService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxNetworkService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, toolboxMigrationService, webHookService, templateService, websiteStatService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo, settingRepo, certRepo)
|
||||
ws := route.NewWs(wsService, toolboxMigrationService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reloader, err := bootstrap.NewTLSReloader(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server, err := bootstrap.NewHttp(config, mux, reloader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
jobs := job.NewJobs(config, db, logger, aggregator, settingRepo, certRepo, certAccountRepo, backupRepo, cacheRepo, taskRepo, scanEventRepo, websiteStatRepo, websiteRepo)
|
||||
cron, err := bootstrap.NewCron(logger, jobs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validation := bootstrap.NewValidator(config, db)
|
||||
ace := app.NewAce(config, mux, server, reloader, gormigrate, cron, taskRunner, validation)
|
||||
return ace, nil
|
||||
}
|
||||
+26
-11
@@ -1,21 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/injector"
|
||||
)
|
||||
|
||||
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 {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("panel must run as root")
|
||||
}
|
||||
|
||||
inj := injector.New()
|
||||
defer func() { _ = inj.Shutdown() }()
|
||||
|
||||
cli, err := do.Invoke[*app.Cli](inj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cli.Run()
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//go:build wireinject
|
||||
|
||||
package main
|
||||
|
||||
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/bootstrap"
|
||||
"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))
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
|
||||
//go:build !wireinject
|
||||
// +build !wireinject
|
||||
|
||||
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/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
)
|
||||
|
||||
import (
|
||||
_ "time/tzdata"
|
||||
)
|
||||
|
||||
// Injectors from wire.go:
|
||||
|
||||
// initCli init command line.
|
||||
func initCli() (*app.Cli, error) {
|
||||
config, err := bootstrap.NewConf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := bootstrap.NewDB(config)
|
||||
if err != nil {
|
||||
return 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
|
||||
}
|
||||
@@ -3,33 +3,30 @@ 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.6.0
|
||||
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.3.0
|
||||
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.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,36 +34,42 @@ 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.1
|
||||
github.com/libtnb/gormstore v1.2.1
|
||||
github.com/libtnb/sessions v1.4.2
|
||||
github.com/libtnb/sqlite v1.1.1
|
||||
github.com/libtnb/cache v1.3.0
|
||||
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.0
|
||||
github.com/libtnb/utils v1.2.2
|
||||
github.com/medama-io/go-useragent v1.2.3
|
||||
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.0
|
||||
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/do/v2 v2.1.0
|
||||
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.9.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.52.0
|
||||
golang.org/x/net v0.55.0
|
||||
gorm.io/gorm v1.31.1
|
||||
modernc.org/sqlite v1.50.1
|
||||
resty.dev/v3 v3.0.0-beta.6
|
||||
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.54.0
|
||||
resty.dev/v3 v3.0.0-rc.3
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -78,29 +81,29 @@ require (
|
||||
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/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.6 // indirect
|
||||
github.com/gofiber/schema v1.7.1 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0 // 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/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
|
||||
@@ -110,8 +113,11 @@ require (
|
||||
github.com/philhofer/fwd v1.2.0 // 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/samber/go-type-to-string v1.8.0 // 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
|
||||
@@ -119,21 +125,22 @@ 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-20260508232706-74f9aab9d74a // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
modernc.org/libc v1.72.3 // 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/text v0.40.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
|
||||
|
||||
@@ -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,13 @@ 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.6.0 h1:0ZuSYc2X/YAVyvEVIfrORonWjC0ZY3x6gZAfe+ugBPM=
|
||||
github.com/bddjr/hlfhr v1.6.0/go.mod h1:cHRve//dit7d8ZCPUUFZAYW5WCm3LdMoiqX9i9GJdms=
|
||||
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=
|
||||
@@ -47,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=
|
||||
@@ -69,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=
|
||||
@@ -86,8 +82,8 @@ 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=
|
||||
@@ -98,18 +94,23 @@ github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj2
|
||||
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.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
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=
|
||||
@@ -124,10 +125,10 @@ github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHl
|
||||
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.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/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=
|
||||
@@ -152,8 +153,6 @@ 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=
|
||||
@@ -162,20 +161,12 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/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=
|
||||
@@ -228,8 +219,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=
|
||||
@@ -253,8 +242,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=
|
||||
@@ -271,26 +260,30 @@ 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.1 h1:5T8L9ka+Lpzp/DOo1+zr8Nou+6bY4wtTq6K4bI/uHV4=
|
||||
github.com/libtnb/cron v0.2.1/go.mod h1:nrOHxS4Ernkn1E//vs64xzC58hGfoL0+lYSW2zsROGI=
|
||||
github.com/libtnb/gormstore v1.2.1 h1:oBRwSsVCa42leVT9GVFP5AzRxtyqj+FhRns9mQwSzEQ=
|
||||
github.com/libtnb/gormstore v1.2.1/go.mod h1:YLsGjjOiUIPhQ6f/ZDb/Vt1ZNI8TXhGZqSzxP1v7vGY=
|
||||
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.2 h1:KEYfuzEW7vtEedwvAZnDWWeW1yMSlwOUfp4hIUirFb8=
|
||||
github.com/libtnb/sessions v1.4.2/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/cache v1.3.0 h1:hrsbC79gdByKeiiH119WUypXnhWg6U+LiP7WCOisgjU=
|
||||
github.com/libtnb/cache v1.3.0/go.mod h1:kArfr9kzJdtryO5FzLVirY5bxFKXf4rok32MK5/F1Ts=
|
||||
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.0 h1:XsA7jsXHH2qmFkTWoy5YCKJybzHzWQ0flqDFP5Y9Yto=
|
||||
github.com/libtnb/sqlite v1.2.0/go.mod h1:O6vURH5fa5IgSmXd/qLAL2zSnYFUd7xSIumsmX3BrSI=
|
||||
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.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=
|
||||
@@ -298,13 +291,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=
|
||||
@@ -318,8 +313,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.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
|
||||
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
|
||||
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=
|
||||
@@ -339,8 +336,8 @@ 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
@@ -358,21 +355,29 @@ 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=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samber/do/v2 v2.1.0 h1:lqCHn05XvY3VqwxvZDQPSkH+jIGWSVHUrSVLEbPOopo=
|
||||
github.com/samber/do/v2 v2.1.0/go.mod h1:wJBoiaZcUZyGuraOhfz15b517ZMogGs+U03DvnqvT6Q=
|
||||
github.com/samber/go-type-to-string v1.8.0 h1:5z6tDTjtXxkIAoAuHAZYMYR8mkBZjVgeSH7jcSLqc8w=
|
||||
github.com/samber/go-type-to-string v1.8.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU=
|
||||
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=
|
||||
@@ -392,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=
|
||||
@@ -412,8 +419,8 @@ 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.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
|
||||
github.com/urfave/cli/v3 v3.9.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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -421,6 +428,12 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
|
||||
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=
|
||||
@@ -428,30 +441,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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
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-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
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=
|
||||
@@ -463,8 +492,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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
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=
|
||||
@@ -479,8 +508,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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
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=
|
||||
@@ -489,8 +518,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=
|
||||
@@ -507,16 +536,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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
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=
|
||||
@@ -536,8 +565,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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
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=
|
||||
@@ -571,28 +600,30 @@ 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.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||
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.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
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=
|
||||
@@ -601,14 +632,14 @@ 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.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
|
||||
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
|
||||
modernc.org/sqlite v1.54.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=
|
||||
|
||||
+10
-10
@@ -13,8 +13,8 @@ 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/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/tlscert"
|
||||
@@ -31,16 +31,16 @@ 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(i do.Injector) (*Ace, error) {
|
||||
return &Ace{
|
||||
conf: conf,
|
||||
router: router,
|
||||
server: server,
|
||||
reloader: reloader,
|
||||
migrator: migrator,
|
||||
cron: cron,
|
||||
runner: runner,
|
||||
}
|
||||
conf: do.MustInvoke[*config.Config](i),
|
||||
router: do.MustInvoke[*chi.Mux](i),
|
||||
server: do.MustInvoke[*hlfhr.Server](i),
|
||||
reloader: do.MustInvoke[*tlscert.Reloader](i),
|
||||
migrator: do.MustInvoke[*gormigrate.Gormigrate](i),
|
||||
cron: do.MustInvoke[*cron.Cron](i),
|
||||
runner: do.MustInvoke[types.TaskRunner](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Ace) Run() error {
|
||||
|
||||
+6
-4
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/gookit/color"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/apploader"
|
||||
@@ -16,12 +17,13 @@ type Cli struct {
|
||||
migrator *gormigrate.Gormigrate
|
||||
}
|
||||
|
||||
func NewCli(cmd *cli.Command, migrator *gormigrate.Gormigrate, _ *apploader.Loader) *Cli {
|
||||
func NewCli(i do.Injector) (*Cli, error) {
|
||||
IsCli = true
|
||||
_ = do.MustInvoke[*apploader.Loader](i) // 强制构造 loader,触发全局应用注册
|
||||
return &Cli{
|
||||
cmd: cmd,
|
||||
migrator: migrator,
|
||||
}
|
||||
cmd: do.MustInvoke[*cli.Command](i),
|
||||
migrator: do.MustInvoke[*gormigrate.Gormigrate](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Cli) Run() error {
|
||||
|
||||
@@ -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,70 @@
|
||||
package app_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/command"
|
||||
"github.com/acepanel/panel/v3/internal/injector"
|
||||
"github.com/acepanel/panel/v3/internal/job"
|
||||
"github.com/acepanel/panel/v3/internal/registry"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
)
|
||||
|
||||
// TestContainer 构建完整对象图,在测试期而非启动期暴露装配错误
|
||||
func TestContainer(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(tmp, "panel/storage/logs"), 0o755))
|
||||
|
||||
// 直接构造内存配置并注入
|
||||
conf := &config.Config{
|
||||
App: config.AppConfig{
|
||||
Debug: true,
|
||||
Key: "12345678901234567890123456789012",
|
||||
Locale: "zh_CN",
|
||||
Timezone: "UTC",
|
||||
Root: tmp,
|
||||
},
|
||||
HTTP: config.HTTPConfig{Port: 8888, TLS: "off"},
|
||||
Session: config.SessionConfig{Lifetime: 120},
|
||||
}
|
||||
require.NoError(t, bootstrap.InitGlobal(conf))
|
||||
|
||||
inj := injector.New()
|
||||
defer func() { _ = inj.Shutdown() }()
|
||||
|
||||
// 覆盖配置提供者,跳过 config.Load 与全局副作用
|
||||
do.OverrideValue(inj, conf)
|
||||
|
||||
// 构造两个入口即触发全图:路由 + SpecJSON + cron 注册(5 段表达式)+ nil 证书重载器
|
||||
_, err := do.Invoke[*app.Ace](inj)
|
||||
require.NoError(t, err)
|
||||
_, err = do.Invoke[*app.Cli](inj)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 每个带冒号的命名贡献必须命中已知前缀(拼错如 route:user 会被静默丢弃)
|
||||
require.NoError(t, registry.Verify(inj, route.RoutePrefix, command.Prefix, job.Prefix))
|
||||
|
||||
routes, err := registry.Collect[route.Endpoints](inj, route.RoutePrefix)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, routes)
|
||||
|
||||
cmds, err := command.Commands(inj)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, cmds)
|
||||
|
||||
jobs, err := registry.Collect[job.Job](inj, job.Prefix)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, jobs)
|
||||
|
||||
spec, err := route.SpecJSON(inj, "AcePanel")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, spec)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -22,10 +23,10 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
+13
-32
@@ -1,7 +1,7 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
@@ -22,6 +22,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"
|
||||
@@ -35,35 +36,15 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/valkey"
|
||||
)
|
||||
|
||||
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,
|
||||
var Package = do.Package(
|
||||
do.Lazy(apache.NewApp), do.Lazy(clickhouse.NewApp), do.Lazy(codeserver.NewApp),
|
||||
do.Lazy(docker.NewApp), do.Lazy(elasticsearch.NewApp), do.Lazy(fail2ban.NewApp),
|
||||
do.Lazy(frp.NewApp), do.Lazy(gitea.NewApp), do.Lazy(grafana.NewApp),
|
||||
do.Lazy(kafka.NewApp), do.Lazy(mariadb.NewApp), do.Lazy(memcached.NewApp),
|
||||
do.Lazy(minio.NewApp), do.Lazy(mongodb.NewApp), do.Lazy(mysql.NewApp),
|
||||
do.Lazy(nginx.NewApp), do.Lazy(openresty.NewApp), do.Lazy(opensearch.NewApp),
|
||||
do.Lazy(percona.NewApp), do.Lazy(pgadmin.NewApp), do.Lazy(phpmyadmin.NewApp), do.Lazy(podman.NewApp),
|
||||
do.Lazy(postgresql.NewApp), do.Lazy(prometheus.NewApp), do.Lazy(pureftpd.NewApp),
|
||||
do.Lazy(redis.NewApp), do.Lazy(rocketmq.NewApp), do.Lazy(rsync.NewApp),
|
||||
do.Lazy(s3fs.NewApp), do.Lazy(supervisor.NewApp), do.Lazy(valkey.NewApp),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
@@ -27,12 +28,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
settingRepo: do.MustInvoke[biz.SettingRepo](i),
|
||||
databaseServerRepo: do.MustInvoke[biz.DatabaseServerRepo](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -66,7 +67,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"`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -13,8 +14,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -16,8 +17,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp(i do.Injector) (*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 更新设置请求
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
@@ -25,8 +26,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{t: do.MustInvoke[*gotext.Locale](i)}, 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
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ 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/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
@@ -27,11 +28,11 @@ type App struct {
|
||||
websiteRepo biz.WebsiteRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, website biz.WebsiteRepo) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
websiteRepo: website,
|
||||
}
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
websiteRepo: do.MustInvoke[biz.WebsiteRepo](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -177,7 +178,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 {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -15,8 +16,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp(i do.Injector) (*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_-]+$\""`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -15,8 +16,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp(i do.Injector) (*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/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
@@ -24,8 +25,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{t: do.MustInvoke[*gotext.Locale](i)}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -67,7 +68,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"`
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -20,8 +21,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{t: do.MustInvoke[*gotext.Locale](i)}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -2,20 +2,23 @@ package mariadb
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"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 {
|
||||
return &App{
|
||||
mysql: mysql.NewApp(t, setting, databaseServer),
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
app, err := mysql.NewApp(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &App{
|
||||
mysql: 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/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -20,10 +21,10 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
}, 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"`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -13,8 +14,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
@@ -26,12 +27,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
settingRepo: do.MustInvoke[biz.SettingRepo](i),
|
||||
databaseServerRepo: do.MustInvoke[biz.DatabaseServerRepo](i),
|
||||
}, 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"`
|
||||
}
|
||||
|
||||
+13
-29
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -28,12 +29,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
settingRepo: do.MustInvoke[biz.SettingRepo](i),
|
||||
databaseServerRepo: do.MustInvoke[biz.DatabaseServerRepo](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -247,30 +248,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 +346,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"`
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
"resty.dev/v3"
|
||||
|
||||
@@ -25,10 +26,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -103,7 +105,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,7 @@ package openresty
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -13,10 +13,10 @@ type App struct {
|
||||
nginx *nginx.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
nginx: nginx.NewApp(t),
|
||||
}
|
||||
nginx: do.MustInvoke[*nginx.App](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
@@ -25,8 +26,9 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
return &App{t: t}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -57,7 +59,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,19 @@ package percona
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"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(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
mysql: mysql.NewApp(t, setting, databaseServer),
|
||||
}
|
||||
mysql: do.MustInvoke[*mysql.App](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
package pgadmin
|
||||
|
||||
import (
|
||||
"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/samber/do/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(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
conf: do.MustInvoke[*config.Config](i),
|
||||
databaseServerRepo: do.MustInvoke[biz.DatabaseServerRepo](i),
|
||||
}, 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(email string) error {
|
||||
servers, _, err := s.databaseServerRepo.List(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(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"`
|
||||
}
|
||||
+181
-16
@@ -1,19 +1,28 @@
|
||||
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/samber/do/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"
|
||||
@@ -22,31 +31,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(i do.Injector) (*App, error) {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
conf: do.MustInvoke[*config.Config](i),
|
||||
databaseServerRepo: do.MustInvoke[biz.DatabaseServerRepo](i),
|
||||
}, 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 +72,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(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"`
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -13,8 +14,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -26,12 +28,15 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
setting := do.MustInvoke[biz.SettingRepo](i)
|
||||
databaseServer := do.MustInvoke[biz.DatabaseServerRepo](i)
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -55,7 +60,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 +77,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 +142,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 +160,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 +213,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("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 +241,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 +288,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 +329,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"`
|
||||
// 内存设置
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
@@ -29,8 +30,11 @@ 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(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
conf := do.MustInvoke[*config.Config](i)
|
||||
taskRepo := do.MustInvoke[biz.TaskRepo](i)
|
||||
return &App{t: t, conf: conf, taskRepo: taskRepo}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -73,7 +77,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 +263,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 +291,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,8 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
@@ -25,10 +26,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -24,11 +25,13 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
databaseServer := do.MustInvoke[biz.DatabaseServerRepo](i)
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -20,8 +21,9 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
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,8 +8,9 @@ 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/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -22,10 +23,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
@@ -23,10 +24,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
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,8 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -22,7 +23,8 @@ type App struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
var name string
|
||||
if os.IsRHEL() {
|
||||
name = "supervisord"
|
||||
@@ -33,7 +35,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"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -24,11 +25,13 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(i do.Injector) (*App, error) {
|
||||
t := do.MustInvoke[*gotext.Locale](i)
|
||||
databaseServer := do.MustInvoke[biz.DatabaseServerRepo](i)
|
||||
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"`
|
||||
}
|
||||
|
||||
+294
-8
@@ -1,8 +1,16 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"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 +26,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(i do.Injector) (*AppUsecase, error) {
|
||||
return &AppUsecase{
|
||||
repo: do.MustInvoke[AppRepo](i),
|
||||
cache: do.MustInvoke[CacheRepo](i),
|
||||
task: do.MustInvoke[TaskRepo](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
}, 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)
|
||||
}
|
||||
|
||||
+90
-9
@@ -2,6 +2,7 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -9,25 +10,105 @@ 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
|
||||
}
|
||||
|
||||
func NewBackupUsecase(repo BackupRepo, log *slog.Logger) *BackupUsecase {
|
||||
return &BackupUsecase{repo: repo, log: log}
|
||||
}
|
||||
|
||||
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 {
|
||||
// 审计留 repo:需区分预检早返回(不记)与备份执行失败(记 Warn),无法在此上移
|
||||
return uc.repo.Create(ctx, typ, target, account)
|
||||
}
|
||||
|
||||
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,16 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"github.com/samber/do/v2"
|
||||
"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 +132,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(i do.Injector) (*BackupAccountUsecase, error) {
|
||||
return &BackupAccountUsecase{
|
||||
repo: do.MustInvoke[BackupAccountRepo](i),
|
||||
setting: do.MustInvoke[SettingRepo](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, 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,22 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/registry"
|
||||
)
|
||||
|
||||
var Package = do.Package(
|
||||
do.Lazy(NewAppUsecase), registry.Lazy2(NewBackupUsecase), do.Lazy(NewBackupAccountUsecase),
|
||||
registry.Lazy(NewCacheUsecase), do.Lazy(NewCertUsecase), do.Lazy(NewCertAccountUsecase),
|
||||
registry.Lazy2(NewCertDNSUsecase), registry.Lazy2(NewContainerUsecase), registry.Lazy(NewContainerComposeUsecase),
|
||||
registry.Lazy2(NewContainerImageUsecase), registry.Lazy2(NewContainerNetworkUsecase), registry.Lazy2(NewContainerVolumeUsecase),
|
||||
registry.Lazy2(NewCronUsecase), do.Lazy(NewDatabaseUsecase), registry.Lazy(NewDatabaseRedisUsecase),
|
||||
registry.Lazy(NewDatabaseElasticsearchUsecase), do.Lazy(NewDatabaseServerUsecase), do.Lazy(NewDatabaseUserUsecase),
|
||||
do.Lazy(NewEnvironmentUsecase), do.Lazy(NewFileShareUsecase), registry.Lazy(NewLogUsecase), registry.Lazy2(NewMonitorUsecase),
|
||||
do.Lazy(NewProjectUsecase), registry.Lazy2(NewSafeUsecase), registry.Lazy2(NewScanEventUsecase),
|
||||
do.Lazy(NewSettingUsecase), registry.Lazy2(NewSSHUsecase), do.Lazy(NewTamperUsecase), registry.Lazy(NewTaskUsecase),
|
||||
do.Lazy(NewTemplateUsecase), do.Lazy(NewUserUsecase), registry.Lazy(NewUserPasskeyUsecase),
|
||||
registry.Lazy(NewUserTokenUsecase), do.Lazy(NewWebHookUsecase), do.Lazy(NewWebsiteUsecase),
|
||||
registry.Lazy(NewWebsiteStatUsecase),
|
||||
)
|
||||
+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.Slugs(), 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))
|
||||
}
|
||||
|
||||
+387
-13
@@ -2,12 +2,22 @@ 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/samber/do/v2"
|
||||
|
||||
"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 +47,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(i do.Injector) (*CertUsecase, error) {
|
||||
return &CertUsecase{
|
||||
repo: do.MustInvoke[CertRepo](i),
|
||||
setting: do.MustInvoke[SettingRepo](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, 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,16 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"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 +30,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(i do.Injector) (*CertAccountUsecase, error) {
|
||||
return &CertAccountUsecase{
|
||||
repo: do.MustInvoke[CertAccountRepo](i),
|
||||
userRepo: do.MustInvoke[UserRepo](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, 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)
|
||||
}
|
||||
|
||||
+200
-5
@@ -2,8 +2,14 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
type DatabaseType string
|
||||
@@ -28,8 +34,197 @@ 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(server *DatabaseServer) ([]*Database, error)
|
||||
Operator(server *DatabaseServer) (db.Operator, error)
|
||||
Mongo(server *DatabaseServer) (*db.MongoDB, error)
|
||||
}
|
||||
|
||||
// DatabaseUsecase 数据库业务用例
|
||||
type DatabaseUsecase struct {
|
||||
repo DatabaseRepo
|
||||
server DatabaseServerRepo
|
||||
user *DatabaseUserUsecase
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseUsecase(i do.Injector) (*DatabaseUsecase, error) {
|
||||
return &DatabaseUsecase{
|
||||
repo: do.MustInvoke[DatabaseRepo](i),
|
||||
server: do.MustInvoke[DatabaseServerRepo](i),
|
||||
user: do.MustInvoke[*DatabaseUserUsecase](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseUsecase) List(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(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(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// MongoDB 独立处理,不走 Operator 接口
|
||||
if server.Type == DatabaseTypeMongoDB {
|
||||
mongo, mongoErr := uc.repo.Mongo(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(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 != "" {
|
||||
if err = operator.PrivilegesGrant(req.Username, req.Name, req.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
|
||||
}
|
||||
|
||||
func (uc *DatabaseUsecase) Delete(ctx context.Context, serverID uint, name string) error {
|
||||
server, err := uc.server.Get(serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case DatabaseTypeMongoDB:
|
||||
mongo, mongoErr := uc.repo.Mongo(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(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(req *request.DatabaseComment) error {
|
||||
server, err := uc.server.Get(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case DatabaseTypePostgresql:
|
||||
operator, opErr := uc.repo.Operator(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,7 +1,7 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
@@ -14,3 +14,40 @@ type DatabaseElasticsearchRepo interface {
|
||||
DocumentSet(req *request.DatabaseESDocumentSet) error
|
||||
DocumentDelete(req *request.DatabaseESDocumentDelete) error
|
||||
}
|
||||
|
||||
// DatabaseElasticsearchUsecase Elasticsearch 业务用例
|
||||
type DatabaseElasticsearchUsecase struct {
|
||||
repo DatabaseElasticsearchRepo
|
||||
}
|
||||
|
||||
func NewDatabaseElasticsearchUsecase(repo DatabaseElasticsearchRepo) *DatabaseElasticsearchUsecase {
|
||||
return &DatabaseElasticsearchUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) Indices(req *request.DatabaseESIndices) ([]db.ESIndex, error) {
|
||||
return uc.repo.Indices(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) IndexCreate(req *request.DatabaseESIndexCreate) error {
|
||||
return uc.repo.IndexCreate(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) IndexDelete(req *request.DatabaseESIndexDelete) error {
|
||||
return uc.repo.IndexDelete(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) Data(req *request.DatabaseESData) ([]db.ESDocument, int64, error) {
|
||||
return uc.repo.Data(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) DocumentGet(req *request.DatabaseESDocumentGet) (*db.ESDocument, error) {
|
||||
return uc.repo.DocumentGet(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) DocumentSet(req *request.DatabaseESDocumentSet) error {
|
||||
return uc.repo.DocumentSet(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseElasticsearchUsecase) DocumentDelete(req *request.DatabaseESDocumentDelete) error {
|
||||
return uc.repo.DocumentDelete(req)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
@@ -15,3 +15,44 @@ type DatabaseRedisRepo interface {
|
||||
KeyRename(req *request.DatabaseRedisKeyRename) error
|
||||
Clear(req *request.DatabaseRedisClear) error
|
||||
}
|
||||
|
||||
// DatabaseRedisUsecase Redis 业务用例
|
||||
type DatabaseRedisUsecase struct {
|
||||
repo DatabaseRedisRepo
|
||||
}
|
||||
|
||||
func NewDatabaseRedisUsecase(repo DatabaseRedisRepo) *DatabaseRedisUsecase {
|
||||
return &DatabaseRedisUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) Databases(req *request.DatabaseRedisDatabases) (int, error) {
|
||||
return uc.repo.Databases(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) Data(req *request.DatabaseRedisData) ([]db.RedisKV, int, error) {
|
||||
return uc.repo.Data(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeyGet(req *request.DatabaseRedisKeyGet) (*db.RedisKV, error) {
|
||||
return uc.repo.KeyGet(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeySet(req *request.DatabaseRedisKeySet) error {
|
||||
return uc.repo.KeySet(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeyDelete(req *request.DatabaseRedisKeyDelete) error {
|
||||
return uc.repo.KeyDelete(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeyTTL(req *request.DatabaseRedisKeyTTL) error {
|
||||
return uc.repo.KeyTTL(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) KeyRename(req *request.DatabaseRedisKeyRename) error {
|
||||
return uc.repo.KeyRename(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseRedisUsecase) Clear(req *request.DatabaseRedisClear) error {
|
||||
return uc.repo.Clear(req)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"github.com/samber/do/v2"
|
||||
"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
|
||||
@@ -65,11 +72,191 @@ type DatabaseServerRepo interface {
|
||||
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
|
||||
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(server *DatabaseServer) (db.Operator, error)
|
||||
CheckServer(server *DatabaseServer) bool
|
||||
}
|
||||
|
||||
// DatabaseServerUsecase 数据库服务器业务用例
|
||||
type DatabaseServerUsecase struct {
|
||||
repo DatabaseServerRepo
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseServerUsecase(i do.Injector) (*DatabaseServerUsecase, error) {
|
||||
return &DatabaseServerUsecase{
|
||||
repo: do.MustInvoke[DatabaseServerRepo](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Count() (int64, error) {
|
||||
return uc.repo.Count()
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) List(page, limit uint, typ string) ([]*DatabaseServer, int64, error) {
|
||||
return uc.repo.List(page, limit, typ)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Get(id uint) (*DatabaseServer, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) GetByName(name string) (*DatabaseServer, error) {
|
||||
return uc.repo.GetByName(name)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Create(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(databaseServer) {
|
||||
return errors.New(uc.t.Get("check server connection failed"))
|
||||
}
|
||||
|
||||
return uc.repo.Create(databaseServer)
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Update(req *request.DatabaseServerUpdate) error {
|
||||
server, err := uc.repo.Get(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(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(id uint) error {
|
||||
server, err := uc.repo.Get(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(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,17 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"github.com/samber/do/v2"
|
||||
"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
|
||||
@@ -66,9 +70,201 @@ 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
|
||||
Operator(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(i do.Injector) (*DatabaseUserUsecase, error) {
|
||||
return &DatabaseUserUsecase{
|
||||
repo: do.MustInvoke[DatabaseUserRepo](i),
|
||||
server: do.MustInvoke[DatabaseServerRepo](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Count() (int64, error) {
|
||||
return uc.repo.Count()
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) List(page, limit uint, typ string) ([]*DatabaseUser, int64, error) {
|
||||
return uc.repo.List(page, limit, typ)
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Get(id uint) (*DatabaseUser, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Create(ctx context.Context, req *request.DatabaseUserCreate) error {
|
||||
server, err := uc.server.Get(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(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(req *request.DatabaseUserUpdate) error {
|
||||
user, err := uc.repo.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := uc.server.Get(user.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(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(req *request.DatabaseUserUpdateRemark) error {
|
||||
return uc.repo.UpdateRemark(req)
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Delete(ctx context.Context, id uint) error {
|
||||
user, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := uc.server.Get(user.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(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(serverID uint, names []string) error {
|
||||
server, err := uc.server.Get(serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := uc.repo.Operator(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)
|
||||
}
|
||||
|
||||
+155
-7
@@ -1,18 +1,166 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"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(i do.Injector) (*EnvironmentUsecase, error) {
|
||||
return &EnvironmentUsecase{
|
||||
repo: do.MustInvoke[EnvironmentRepo](i),
|
||||
cache: do.MustInvoke[CacheRepo](i),
|
||||
task: do.MustInvoke[TaskRepo](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
}, 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,85 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"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(i do.Injector) (*FileShareUsecase, error) {
|
||||
return &FileShareUsecase{
|
||||
repo: do.MustInvoke[FileShareRepo](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, 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
|
||||
}
|
||||
@@ -52,3 +52,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)
|
||||
}
|
||||
|
||||
+56
-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,59 @@ 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
|
||||
}
|
||||
|
||||
setting := new(request.MonitorSetting)
|
||||
setting.Enabled = cast.ToBool(monitor)
|
||||
setting.Days = cast.ToUint(monitorDays)
|
||||
setting.Interval = uint(monitorInterval)
|
||||
|
||||
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 nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
+140
-6
@@ -2,9 +2,17 @@ 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/do/v2"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -19,9 +27,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(i do.Injector) (*ProjectUsecase, error) {
|
||||
return &ProjectUsecase{
|
||||
repo: do.MustInvoke[ProjectRepo](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
}, 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()
|
||||
}
|
||||
|
||||
+255
-3
@@ -2,9 +2,23 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/cert"
|
||||
"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/os"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
)
|
||||
|
||||
type SettingKey string
|
||||
@@ -50,6 +64,11 @@ const (
|
||||
SettingKeyIPDBType SettingKey = "ipdb_type" // "" / "custom" / "subscribe"
|
||||
SettingKeyIPDBURL SettingKey = "ipdb_url" // 订阅链接
|
||||
SettingKeyIPDBPath SettingKey = "ipdb_path"
|
||||
SettingKeyInfoRan SettingKey = "info_ran" // info 命令是否已运行过
|
||||
SettingKeyTamperEnabled SettingKey = "tamper_enabled"
|
||||
SettingKeyTamperMode SettingKey = "tamper_mode" // chattr / ebpf
|
||||
SettingKeyTamperBlockNew SettingKey = "tamper_block_new" // 新建受保护类型文件时删除拦截
|
||||
SettingKeyTamperLogDays SettingKey = "tamper_log_days" // 拦截日志保留天数
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
@@ -69,6 +88,239 @@ type SettingRepo interface {
|
||||
SetSlice(key SettingKey, value []string) error
|
||||
Delete(key SettingKey) error
|
||||
GetPanel() (*request.SettingPanel, error)
|
||||
UpdatePanel(ctx context.Context, req *request.SettingPanel) (bool, error)
|
||||
UpdateCert(req *request.SettingCert) error
|
||||
}
|
||||
|
||||
type SettingUsecase struct {
|
||||
repo SettingRepo
|
||||
task TaskRepo
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewSettingUsecase(i do.Injector) (*SettingUsecase, error) {
|
||||
return &SettingUsecase{
|
||||
repo: do.MustInvoke[SettingRepo](i),
|
||||
task: do.MustInvoke[TaskRepo](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) Get(key SettingKey, defaultValue ...string) (string, error) {
|
||||
return uc.repo.Get(key, defaultValue...)
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) GetBool(key SettingKey, defaultValue ...bool) (bool, error) {
|
||||
return uc.repo.GetBool(key, defaultValue...)
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) GetInt(key SettingKey, defaultValue ...int) (int, error) {
|
||||
return uc.repo.GetInt(key, defaultValue...)
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) GetSlice(key SettingKey, defaultValue ...[]string) ([]string, error) {
|
||||
return uc.repo.GetSlice(key, defaultValue...)
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) Set(key SettingKey, value string) error {
|
||||
return uc.repo.Set(key, value)
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) SetSlice(key SettingKey, value []string) error {
|
||||
return uc.repo.SetSlice(key, value)
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) Delete(key SettingKey) error {
|
||||
return uc.repo.Delete(key)
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) GetPanel() (*request.SettingPanel, error) {
|
||||
return uc.repo.GetPanel()
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) UpdatePanel(ctx context.Context, req *request.SettingPanel) (bool, error) {
|
||||
if err := uc.repo.Set(SettingKeyName, req.Name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyChannel, req.Channel); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyOfflineMode, cast.ToString(req.OfflineMode)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyAutoUpdate, cast.ToString(req.AutoUpdate)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyWebsitePath, req.WebsitePath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyBackupPath, req.BackupPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyBackupFormat, req.BackupFormat); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyProjectPath, req.ProjectPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyContainerSock, req.ContainerSock); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.SetSlice(SettingHiddenMenu, req.HiddenMenu); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyCustomLogo, req.CustomLogo); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyIPDBType, req.IPDBType); err != nil {
|
||||
return false, err
|
||||
}
|
||||
ipdbURL := req.IPDBURL
|
||||
if req.IPDBType == "subscribe" && ipdbURL == "" {
|
||||
// https://github.com/metowolf/qqwry.ipdb
|
||||
ipdbURL = "https://fastly.jsdelivr.net/npm/qqwry.ipdb/qqwry.ipdb"
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyIPDBURL, ipdbURL); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.Set(SettingKeyIPDBPath, req.IPDBPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := uc.repo.SetSlice(SettingKeyPublicIPs, req.PublicIP); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 订阅模式后台下载 IPDB
|
||||
if req.IPDBType == "subscribe" && ipdbURL != "" {
|
||||
go func() {
|
||||
destPath := filepath.Join(app.Root, "panel/storage/geo.ipdb")
|
||||
if err := io.DownloadFile(ipdbURL, destPath); err != nil {
|
||||
uc.log.Warn("failed to download ipdb", slog.String("url", ipdbURL), slog.Any("err", err))
|
||||
} else {
|
||||
uc.log.Info("ipdb downloaded", slog.String("url", ipdbURL))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 下面是需要需要重启的设置
|
||||
// 面板HTTPS
|
||||
restartFlag := false
|
||||
|
||||
// 自签模式
|
||||
if req.TLS == "self-signed" {
|
||||
needGen := req.Cert == "" || req.Key == ""
|
||||
if !needGen {
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
needGen = true
|
||||
}
|
||||
}
|
||||
if needGen {
|
||||
crt, key, err := cert.GenerateSelfSigned(tools.CollectLocalNames())
|
||||
if err != nil {
|
||||
return false, errors.New(uc.t.Get("failed to generate self-signed certificate: %v", err))
|
||||
}
|
||||
req.Cert = string(crt)
|
||||
req.Key = string(key)
|
||||
}
|
||||
}
|
||||
|
||||
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
|
||||
if oldCert != req.Cert || oldKey != req.Key {
|
||||
if uc.task.HasRunningTask() {
|
||||
return false, errors.New(uc.t.Get("background task is running, modifying some settings is prohibited, please try again later"))
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
// custom 模式需要验证证书格式
|
||||
if req.TLS == "custom" {
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
return false, errors.New(uc.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err := cert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return false, errors.New(uc.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 面板主配置
|
||||
conf, err := config.Load()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if req.Port != conf.HTTP.Port {
|
||||
if os.TCPPortInUse(req.Port) {
|
||||
return false, errors.New(uc.t.Get("port is already in use"))
|
||||
}
|
||||
// 放行端口
|
||||
fw := firewall.NewFirewall()
|
||||
if ok, _ := fw.Status(); ok {
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Protocol: firewall.ProtocolTCPUDP,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conf.App.Locale = req.Locale
|
||||
conf.HTTP.Port = req.Port
|
||||
conf.HTTP.Entrance = req.Entrance
|
||||
conf.HTTP.EntranceError = req.EntranceError
|
||||
conf.HTTP.LoginCaptcha = req.LoginCaptcha
|
||||
conf.HTTP.TLS = req.TLS
|
||||
conf.HTTP.IPHeader = req.IPHeader
|
||||
conf.HTTP.BindDomain = req.BindDomain
|
||||
conf.HTTP.BindIP = req.BindIP
|
||||
conf.HTTP.BindUA = req.BindUA
|
||||
conf.Session.Lifetime = req.Lifetime
|
||||
|
||||
// 检查配置是否有变更
|
||||
if same, _ := config.Check(conf); !same {
|
||||
if uc.task.HasRunningTask() {
|
||||
return false, errors.New(uc.t.Get("background task is running, modifying some settings is prohibited, please try again later"))
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
if err = config.Save(conf); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("panel settings updated", slog.String("type", OperationTypeSetting), slog.Uint64("operator_id", operatorID(ctx)))
|
||||
|
||||
return restartFlag, nil
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) UpdateCert(req *request.SettingCert) error {
|
||||
if uc.task.HasRunningTask() {
|
||||
return errors.New(uc.t.Get("background task is running, modifying some settings is prohibited, please try again later"))
|
||||
}
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
return errors.New(uc.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err := cert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return errors.New(uc.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+94
-4
@@ -2,13 +2,14 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"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/ssh"
|
||||
)
|
||||
|
||||
@@ -59,10 +60,99 @@ func (r *SSH) AfterFind(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHFileInfo SFTP 文件信息
|
||||
type SSHFileInfo struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Mode string `json:"mode"`
|
||||
ModTime int64 `json:"mod_time"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
IsLink bool `json:"is_link"`
|
||||
}
|
||||
|
||||
type SSHRepo interface {
|
||||
List(page, limit uint) ([]*SSH, int64, error)
|
||||
Get(id uint) (*SSH, error)
|
||||
Create(ctx context.Context, req *request.SSHCreate) error
|
||||
Update(ctx context.Context, req *request.SSHUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
Create(req *request.SSHCreate) error
|
||||
Update(req *request.SSHUpdate) error
|
||||
Delete(id uint) error
|
||||
// 文件互传相关,hostID 为 0 表示面板本机
|
||||
ListFiles(hostID uint, path string) ([]*SSHFileInfo, error)
|
||||
Mkdir(hostID uint, path string) error
|
||||
TransferFile(ctx context.Context, srcID uint, srcPath string, dstID uint, dstPath string, progress func(transferred, total int64)) error
|
||||
}
|
||||
|
||||
// SSHUsecase SSH 业务逻辑
|
||||
type SSHUsecase struct {
|
||||
repo SSHRepo
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewSSHUsecase(repo SSHRepo, log *slog.Logger) *SSHUsecase {
|
||||
return &SSHUsecase{repo: repo, log: log}
|
||||
}
|
||||
|
||||
func (uc *SSHUsecase) List(page, limit uint) ([]*SSH, int64, error) {
|
||||
return uc.repo.List(page, limit)
|
||||
}
|
||||
|
||||
func (uc *SSHUsecase) Get(id uint) (*SSH, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *SSHUsecase) Create(ctx context.Context, req *request.SSHCreate) error {
|
||||
if err := uc.repo.Create(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("ssh created", slog.String("type", OperationTypeSSH), slog.Uint64("operator_id", operatorID(ctx)), slog.String("name", req.Name), slog.String("host", req.Host))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *SSHUsecase) Update(ctx context.Context, req *request.SSHUpdate) error {
|
||||
if err := uc.repo.Update(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("ssh updated", slog.String("type", OperationTypeSSH), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", req.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *SSHUsecase) Delete(ctx context.Context, id uint) error {
|
||||
s, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("ssh deleted", slog.String("type", OperationTypeSSH), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", s.Name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *SSHUsecase) ListFiles(hostID uint, path string) ([]*SSHFileInfo, error) {
|
||||
return uc.repo.ListFiles(hostID, path)
|
||||
}
|
||||
|
||||
func (uc *SSHUsecase) Mkdir(hostID uint, path string) error {
|
||||
return uc.repo.Mkdir(hostID, path)
|
||||
}
|
||||
|
||||
func (uc *SSHUsecase) TransferFile(ctx context.Context, srcID uint, srcPath string, dstID uint, dstPath string, progress func(transferred, total int64)) error {
|
||||
if err := uc.repo.TransferFile(ctx, srcID, srcPath, dstID, dstPath, progress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("ssh file transferred", slog.String("type", OperationTypeSSH), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("src_id", uint64(srcID)), slog.String("src_path", srcPath), slog.Uint64("dst_id", uint64(dstID)), slog.String("dst_path", dstPath))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/tamper"
|
||||
)
|
||||
|
||||
// TamperRule 防篡改保护规则(通常对应一个网站目录)
|
||||
type TamperRule struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;default:'';unique" json:"name"` // 标识
|
||||
Path string `gorm:"not null;default:''" json:"path"` // 受保护目录
|
||||
Exts []string `gorm:"serializer:json" json:"exts"` // 受保护后缀,空=全部
|
||||
Excludes []string `gorm:"serializer:json" json:"excludes"` // 排除子路径
|
||||
Enabled bool `gorm:"not null;default:true" json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TamperLog 篡改拦截/告警日志
|
||||
type TamperLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Path string `gorm:"not null;default:'';index" json:"path"`
|
||||
Op string `gorm:"not null;default:''" json:"op"` // write/unlink/rename/setattr/create
|
||||
PID uint `gorm:"not null;default:0" json:"pid"`
|
||||
Comm string `gorm:"not null;default:''" json:"comm"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
}
|
||||
|
||||
// TamperSetting 防篡改全局设置
|
||||
type TamperSetting struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Mode string `json:"mode"` // chattr / ebpf
|
||||
BlockNewFiles bool `json:"block_new_files"` // 新建受保护类型文件时删除拦截
|
||||
LogDays uint `json:"log_days"` // 日志保留天数
|
||||
}
|
||||
|
||||
// TamperRepo 防篡改数据访问接口
|
||||
type TamperRepo interface {
|
||||
ListRules() ([]*TamperRule, error)
|
||||
GetRule(id uint) (*TamperRule, error)
|
||||
GetRuleByName(name string) (*TamperRule, error)
|
||||
CreateRule(rule *TamperRule) error
|
||||
UpdateRule(rule *TamperRule) error
|
||||
DeleteRule(id uint) error
|
||||
AddLogs(logs []*TamperLog) error
|
||||
ListLogs(page, limit uint) ([]*TamperLog, int64, error)
|
||||
ClearLogs() error
|
||||
ClearLogsBefore(t time.Time) error
|
||||
VacuumDB() error
|
||||
}
|
||||
|
||||
// TamperUsecase 防篡改业务逻辑,持有运行时 Manager
|
||||
type TamperUsecase struct {
|
||||
repo TamperRepo
|
||||
setting *SettingUsecase
|
||||
log *slog.Logger
|
||||
|
||||
mu sync.Mutex
|
||||
mgr *tamper.Manager
|
||||
buf []*TamperLog
|
||||
bufMu sync.Mutex
|
||||
drainC chan struct{}
|
||||
}
|
||||
|
||||
func NewTamperUsecase(i do.Injector) (*TamperUsecase, error) {
|
||||
return &TamperUsecase{
|
||||
repo: do.MustInvoke[TamperRepo](i),
|
||||
setting: do.MustInvoke[*SettingUsecase](i),
|
||||
log: do.MustInvoke[*slog.Logger](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Supported 当前平台是否支持防篡改
|
||||
func (uc *TamperUsecase) Supported() bool {
|
||||
return tamper.Supported()
|
||||
}
|
||||
|
||||
// DetectEBPF 检测 eBPF 模式可用性
|
||||
func (uc *TamperUsecase) DetectEBPF() tamper.EBPFStatus {
|
||||
return tamper.DetectEBPF()
|
||||
}
|
||||
|
||||
// EnableBPFLSMGrub 修改 grub 激活 bpf LSM(需重启系统生效)
|
||||
func (uc *TamperUsecase) EnableBPFLSMGrub() error {
|
||||
return tamper.EnableBPFLSMGrub()
|
||||
}
|
||||
|
||||
// GetSetting 读取全局设置
|
||||
func (uc *TamperUsecase) GetSetting() (*TamperSetting, error) {
|
||||
enabled, _ := uc.setting.GetBool(SettingKeyTamperEnabled)
|
||||
mode, _ := uc.setting.Get(SettingKeyTamperMode)
|
||||
if mode == "" {
|
||||
mode = string(tamper.ModeChattr)
|
||||
}
|
||||
blockNew, _ := uc.setting.GetBool(SettingKeyTamperBlockNew)
|
||||
logDays, _ := uc.setting.GetInt(SettingKeyTamperLogDays, 30)
|
||||
return &TamperSetting{
|
||||
Enabled: enabled,
|
||||
Mode: mode,
|
||||
BlockNewFiles: blockNew,
|
||||
LogDays: uint(logDays),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SaveSetting 保存全局设置并立即生效
|
||||
func (uc *TamperUsecase) SaveSetting(s *TamperSetting) error {
|
||||
if err := uc.setting.Set(SettingKeyTamperMode, s.Mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := uc.setting.Set(SettingKeyTamperBlockNew, cast.ToString(s.BlockNewFiles)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := uc.setting.Set(SettingKeyTamperLogDays, cast.ToString(s.LogDays)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := uc.setting.Set(SettingKeyTamperEnabled, cast.ToString(s.Enabled)); err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.Reconcile()
|
||||
}
|
||||
|
||||
// Rules 规则管理
|
||||
func (uc *TamperUsecase) ListRules() ([]*TamperRule, error) { return uc.repo.ListRules() }
|
||||
func (uc *TamperUsecase) GetRule(id uint) (*TamperRule, error) { return uc.repo.GetRule(id) }
|
||||
|
||||
func (uc *TamperUsecase) CreateRule(rule *TamperRule) error {
|
||||
if err := uc.repo.CreateRule(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.Reconcile()
|
||||
}
|
||||
|
||||
func (uc *TamperUsecase) UpdateRule(rule *TamperRule) error {
|
||||
if err := uc.repo.UpdateRule(rule); err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.Reconcile()
|
||||
}
|
||||
|
||||
func (uc *TamperUsecase) DeleteRule(id uint) error {
|
||||
if err := uc.repo.DeleteRule(id); err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.Reconcile()
|
||||
}
|
||||
|
||||
// Logs 日志
|
||||
func (uc *TamperUsecase) ListLogs(page, limit uint) ([]*TamperLog, int64, error) {
|
||||
// 先落缓冲,拦截日志即时可见
|
||||
uc.FlushLogs()
|
||||
return uc.repo.ListLogs(page, limit)
|
||||
}
|
||||
func (uc *TamperUsecase) ClearLogs() error { return uc.repo.ClearLogs() }
|
||||
|
||||
// buildConfig 从设置与规则构造运行配置
|
||||
func (uc *TamperUsecase) buildConfig(s *TamperSetting) (tamper.Config, error) {
|
||||
rules, err := uc.repo.ListRules()
|
||||
if err != nil {
|
||||
return tamper.Config{}, err
|
||||
}
|
||||
cfg := tamper.Config{
|
||||
Mode: tamper.Mode(s.Mode),
|
||||
BlockNewFiles: s.BlockNewFiles,
|
||||
}
|
||||
for _, r := range rules {
|
||||
if !r.Enabled || r.Path == "" {
|
||||
continue
|
||||
}
|
||||
cfg.Rules = append(cfg.Rules, tamper.Rule{
|
||||
Name: r.Name,
|
||||
Paths: []string{r.Path},
|
||||
Exts: r.Exts,
|
||||
Excludes: r.Excludes,
|
||||
})
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Reconcile 依据设置启停并同步 Manager,应由设置/规则变更后调用
|
||||
func (uc *TamperUsecase) Reconcile() error {
|
||||
s, err := uc.GetSetting()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
|
||||
// 未启用或无可用规则:停止
|
||||
if !s.Enabled {
|
||||
uc.stopLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg, err := uc.buildConfig(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(cfg.Rules) == 0 {
|
||||
uc.stopLocked()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 重启 Manager 以套用最新配置(规则/模式可能变化)
|
||||
uc.stopLocked()
|
||||
mgr, err := tamper.NewManager(cfg, uc.log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = mgr.Start(); err != nil {
|
||||
_ = mgr.Stop()
|
||||
return err
|
||||
}
|
||||
uc.mgr = mgr
|
||||
uc.drainC = make(chan struct{})
|
||||
go uc.drain(mgr, uc.drainC)
|
||||
return nil
|
||||
}
|
||||
|
||||
// stopLocked 停止当前 Manager(调用方持锁)
|
||||
func (uc *TamperUsecase) stopLocked() {
|
||||
if uc.mgr != nil {
|
||||
if uc.drainC != nil {
|
||||
close(uc.drainC)
|
||||
uc.drainC = nil
|
||||
}
|
||||
_ = uc.mgr.Stop()
|
||||
uc.mgr = nil
|
||||
}
|
||||
}
|
||||
|
||||
// drain 持续读取拦截事件缓冲到内存
|
||||
func (uc *TamperUsecase) drain(mgr *tamper.Manager, done chan struct{}) {
|
||||
ch := mgr.Events()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case ev, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
uc.bufMu.Lock()
|
||||
uc.buf = append(uc.buf, &TamperLog{
|
||||
Path: ev.Path,
|
||||
Op: ev.OpStr,
|
||||
PID: uint(ev.PID),
|
||||
Comm: ev.Comm,
|
||||
CreatedAt: ev.Time,
|
||||
})
|
||||
uc.bufMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FlushLogs 将缓冲的拦截日志落库,由定时任务调用
|
||||
func (uc *TamperUsecase) FlushLogs() {
|
||||
uc.bufMu.Lock()
|
||||
if len(uc.buf) == 0 {
|
||||
uc.bufMu.Unlock()
|
||||
return
|
||||
}
|
||||
logs := uc.buf
|
||||
uc.buf = nil
|
||||
uc.bufMu.Unlock()
|
||||
|
||||
if err := uc.repo.AddLogs(logs); err != nil {
|
||||
uc.log.Warn("failed to persist tamper logs", slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupLogs 清理过期日志
|
||||
func (uc *TamperUsecase) CleanupLogs() {
|
||||
s, err := uc.GetSetting()
|
||||
if err != nil || s.LogDays == 0 {
|
||||
return
|
||||
}
|
||||
_ = uc.repo.ClearLogsBefore(time.Now().AddDate(0, 0, -int(s.LogDays)))
|
||||
}
|
||||
|
||||
// Running 当前是否有 Manager 在运行
|
||||
func (uc *TamperUsecase) Running() bool {
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
return uc.mgr != nil
|
||||
}
|
||||
|
||||
// Stats 运行统计
|
||||
func (uc *TamperUsecase) Stats() tamper.Stats {
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
if uc.mgr == nil {
|
||||
return tamper.Stats{}
|
||||
}
|
||||
return uc.mgr.Stats()
|
||||
}
|
||||
|
||||
// enabledRules 取启用规则并转为 tamper.Rule
|
||||
func (uc *TamperUsecase) enabledRules() ([]*TamperRule, []tamper.Rule, error) {
|
||||
rules, err := uc.repo.ListRules()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var converted []tamper.Rule
|
||||
for _, r := range rules {
|
||||
if !r.Enabled || r.Path == "" {
|
||||
continue
|
||||
}
|
||||
converted = append(converted, tamper.Rule{
|
||||
Name: r.Name,
|
||||
Paths: []string{r.Path},
|
||||
Exts: r.Exts,
|
||||
Excludes: r.Excludes,
|
||||
})
|
||||
}
|
||||
return rules, converted, nil
|
||||
}
|
||||
|
||||
// CheckPaths 批量查询路径是否处于保护范围(防篡改未运行时全为 false)
|
||||
func (uc *TamperUsecase) CheckPaths(paths []string) (map[string]bool, error) {
|
||||
res := make(map[string]bool, len(paths))
|
||||
for _, p := range paths {
|
||||
res[p] = false
|
||||
}
|
||||
if !uc.Running() {
|
||||
return res, nil
|
||||
}
|
||||
_, rules, err := uc.enabledRules()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range paths {
|
||||
info, err := os.Lstat(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
res[p] = tamper.Covered(rules, p, info.IsDir())
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SetProtect 切换单个路径的保护状态,经由规则/排除联动实现:
|
||||
// 保护:命中排除则移除排除项,无覆盖规则则对目录新建整树规则
|
||||
// 解除:路径即规则根则删除规则,否则加入排除项
|
||||
func (uc *TamperUsecase) SetProtect(path string, protect bool) error {
|
||||
path = filepath.Clean(path)
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isDir := info.IsDir()
|
||||
|
||||
rules, _, err := uc.enabledRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var covering *TamperRule
|
||||
for _, r := range rules {
|
||||
if r.Enabled && r.Path != "" && tamper.UnderRoot(path, filepath.Clean(r.Path)) {
|
||||
covering = r
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if protect {
|
||||
if covering == nil {
|
||||
if !isDir {
|
||||
return errors.New("file is not inside any protection rule, protect its directory instead")
|
||||
}
|
||||
return uc.CreateRule(&TamperRule{Name: uc.ruleNameFor(rules, path), Path: path, Enabled: true})
|
||||
}
|
||||
// 移除导致该路径被排除的排除项
|
||||
kept := make([]string, 0, len(covering.Excludes))
|
||||
for _, ex := range covering.Excludes {
|
||||
if !tamper.ExcludeMatches(ex, path) {
|
||||
kept = append(kept, ex)
|
||||
}
|
||||
}
|
||||
if len(kept) != len(covering.Excludes) {
|
||||
covering.Excludes = kept
|
||||
return uc.UpdateRule(covering)
|
||||
}
|
||||
if !isDir && !tamper.MatchExt(path, covering.Exts) {
|
||||
return errors.New("file extension is not protected by the rule, edit the rule extensions instead")
|
||||
}
|
||||
return nil // 已处于保护中
|
||||
}
|
||||
|
||||
if covering == nil {
|
||||
return nil // 本就未受保护
|
||||
}
|
||||
if path == filepath.Clean(covering.Path) {
|
||||
return uc.DeleteRule(covering.ID)
|
||||
}
|
||||
if !tamper.Covered([]tamper.Rule{{Paths: []string{covering.Path}, Exts: covering.Exts, Excludes: covering.Excludes}}, path, isDir) {
|
||||
return nil // 本就未受保护(已排除或后缀不匹配)
|
||||
}
|
||||
covering.Excludes = append(covering.Excludes, path)
|
||||
return uc.UpdateRule(covering)
|
||||
}
|
||||
|
||||
// ruleNameFor 生成不冲突的规则名
|
||||
func (uc *TamperUsecase) ruleNameFor(rules []*TamperRule, path string) string {
|
||||
name := filepath.Base(path)
|
||||
for _, r := range rules {
|
||||
if r.Name == name {
|
||||
return strings.Trim(strings.ReplaceAll(path, "/", "-"), "-")
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// Unlock 临时解除指定路径保护(供面板合法写入前调用),返回是否处于保护中
|
||||
func (uc *TamperUsecase) Unlock(paths ...string) bool {
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
if uc.mgr == nil {
|
||||
return false
|
||||
}
|
||||
uc.mgr.Unlock(paths)
|
||||
return true
|
||||
}
|
||||
|
||||
// Relock 恢复指定路径保护
|
||||
func (uc *TamperUsecase) Relock(paths ...string) {
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
if uc.mgr != nil {
|
||||
uc.mgr.Relock(paths)
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *TamperUsecase) VacuumDB() error {
|
||||
return uc.repo.VacuumDB()
|
||||
}
|
||||
+55
-11
@@ -5,20 +5,23 @@ import "time"
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
TaskStatusWaiting TaskStatus = "waiting"
|
||||
TaskStatusRunning TaskStatus = "running"
|
||||
TaskStatusSuccess TaskStatus = "finished"
|
||||
TaskStatusFailed TaskStatus = "failed"
|
||||
TaskStatusWaiting TaskStatus = "waiting"
|
||||
TaskStatusRunning TaskStatus = "running"
|
||||
TaskStatusSuccess TaskStatus = "finished"
|
||||
TaskStatusFailed TaskStatus = "failed"
|
||||
TaskStatusCanceled TaskStatus = "canceled"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;default:'';index" json:"name"`
|
||||
Status TaskStatus `gorm:"not null;default:'waiting'" json:"status"`
|
||||
Shell string `gorm:"not null;default:''" json:"-"`
|
||||
Log string `gorm:"not null;default:''" json:"log"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Key string `gorm:"not null;default:'';index" json:"-"` // 任务标识(域:动作:标的),用于防重
|
||||
Name string `gorm:"not null;default:'';index" json:"name"`
|
||||
Status TaskStatus `gorm:"not null;default:'waiting'" json:"status"`
|
||||
Shell string `gorm:"not null;default:''" json:"-"`
|
||||
CancelShell string `gorm:"not null;default:''" json:"-"` // 运行中被取消后执行的清理命令(可选)
|
||||
Log string `gorm:"not null;default:''" json:"log"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TaskRepo interface {
|
||||
@@ -26,7 +29,48 @@ type TaskRepo interface {
|
||||
List(page, limit uint) ([]*Task, int64, error)
|
||||
Get(id uint) (*Task, error)
|
||||
Delete(id uint) error
|
||||
Cancel(id uint) error
|
||||
UpdateStatus(id uint, status TaskStatus) error
|
||||
UpdateLog(id uint, log string) error
|
||||
Push(task *Task) error
|
||||
}
|
||||
|
||||
type TaskUsecase struct {
|
||||
repo TaskRepo
|
||||
}
|
||||
|
||||
func NewTaskUsecase(repo TaskRepo) *TaskUsecase {
|
||||
return &TaskUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) HasRunningTask() bool {
|
||||
return uc.repo.HasRunningTask()
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) List(page, limit uint) ([]*Task, int64, error) {
|
||||
return uc.repo.List(page, limit)
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) Get(id uint) (*Task, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) Delete(id uint) error {
|
||||
return uc.repo.Delete(id)
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) Cancel(id uint) error {
|
||||
return uc.repo.Cancel(id)
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) UpdateStatus(id uint, status TaskStatus) error {
|
||||
return uc.repo.UpdateStatus(id, status)
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) UpdateLog(id uint, log string) error {
|
||||
return uc.repo.UpdateLog(id, log)
|
||||
}
|
||||
|
||||
func (uc *TaskUsecase) Push(task *Task) error {
|
||||
return uc.repo.Push(task)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,103 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/pkg/api"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type TemplateRepo interface {
|
||||
List() api.Templates
|
||||
Get(slug string) (*api.Template, error)
|
||||
LoadLocalTemplates() api.Templates
|
||||
Callback(slug string) error
|
||||
CreateCompose(name, compose string, envs []types.KV, autoFirewall bool) (string, error)
|
||||
WriteCompose(name, compose string, envs []types.KV) (string, error)
|
||||
OpenComposePorts(compose string) error
|
||||
}
|
||||
|
||||
type TemplateUsecase struct {
|
||||
repo TemplateRepo
|
||||
cache CacheRepo
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewTemplateUsecase(i do.Injector) (*TemplateUsecase, error) {
|
||||
return &TemplateUsecase{
|
||||
repo: do.MustInvoke[TemplateRepo](i),
|
||||
cache: do.MustInvoke[CacheRepo](i),
|
||||
t: do.MustInvoke[*gotext.Locale](i),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List 获取所有模版,包括本地模板
|
||||
func (uc *TemplateUsecase) List() api.Templates {
|
||||
templates := make(api.Templates, 0)
|
||||
cached, err := uc.cache.Get(CacheKeyTemplates)
|
||||
if err == nil {
|
||||
_ = json.Unmarshal([]byte(cached), &templates)
|
||||
}
|
||||
|
||||
// 加载本地模板并合并,本地模板覆盖同 slug 的远端模板
|
||||
localTemplates := uc.repo.LoadLocalTemplates()
|
||||
if len(localTemplates) > 0 {
|
||||
slugMap := make(map[string]int, len(templates))
|
||||
for i, t := range templates {
|
||||
slugMap[t.Slug] = i
|
||||
}
|
||||
for _, lt := range localTemplates {
|
||||
if i, ok := slugMap[lt.Slug]; ok {
|
||||
templates[i] = lt
|
||||
} else {
|
||||
templates = append(templates, lt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return templates
|
||||
}
|
||||
|
||||
// Get 获取模版详情
|
||||
func (uc *TemplateUsecase) Get(slug string) (*api.Template, error) {
|
||||
templates := uc.List()
|
||||
|
||||
for _, t := range templates {
|
||||
if t.Slug == slug {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New(uc.t.Get("template %s not found", slug))
|
||||
}
|
||||
|
||||
// Callback 模版下载回调
|
||||
func (uc *TemplateUsecase) Callback(slug string) error {
|
||||
return uc.repo.Callback(slug)
|
||||
}
|
||||
|
||||
// CreateCompose 创建编排
|
||||
func (uc *TemplateUsecase) CreateCompose(name, compose string, envs []types.KV, autoFirewall bool) (string, error) {
|
||||
dir := filepath.Join(app.Root, "compose", name)
|
||||
|
||||
// 检查编排是否已存在
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
return "", errors.New(uc.t.Get("compose %s already exists", name))
|
||||
}
|
||||
|
||||
dir, err := uc.repo.WriteCompose(name, compose, envs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 自动放行端口
|
||||
if autoFirewall {
|
||||
_ = uc.repo.OpenComposePorts(compose)
|
||||
}
|
||||
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user