From c4bf7ee03b9209f484eca9e3a0fca450d095e001 Mon Sep 17 00:00:00 2001 From: yvan <8574526@qq.com> Date: Thu, 13 Aug 2026 23:17:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=BA=90=E5=92=8C=20oss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- cmd/kratos-admin/main.go | 99 ++++ cmd/kratos-admin/wire.go | 24 + cmd/kratos-admin/wire_gen.go | 77 +++ configs/config.yaml | 16 +- go.mod | 52 +- go.sum | 226 +++++++- internal/biz/system.go | 25 +- internal/conf/conf.pb.go | 522 ++++++++++++++++-- internal/conf/conf.proto | 46 ++ internal/data/admin.go | 142 ++--- internal/data/config_store.go | 103 ++++ internal/data/data.go | 103 ++-- internal/data/database.go | 179 ++++++ internal/data/file_storage.go | 99 ++-- internal/data/media.go | 2 +- internal/data/qiniu_storage.go | 126 +++++ internal/data/s3_storage.go | 138 +++++ internal/data/settings.go | 5 +- internal/data/system.go | 23 +- internal/server/audit_middleware.go | 2 +- internal/server/gin.go | 40 +- internal/server/system_config_routes.go | 67 ++- internal/service/system.go | 25 +- web/package.json | 2 - web/pnpm-lock.yaml | 61 -- web/src/api/api.js | 8 - web/src/api/system/sysError.js | 6 - web/src/components/charts/index.vue | 47 -- web/src/core/componentLibrary/index.js | 2 +- web/src/core/global.js | 1 - .../components/charts-content-numbers.vue | 187 ------- .../components/charts-people-numbers.vue | 132 ----- web/src/view/dashboard/components/charts.vue | 49 -- web/src/view/dashboard/components/index.js | 2 - web/src/view/dashboard/components/notice.vue | 41 +- web/src/view/dashboard/index.vue | 93 +--- web/src/view/layout/header/tools.vue | 69 --- web/src/view/systemTools/system/system.vue | 52 +- web/uno.config.js | 1 - 40 files changed, 1975 insertions(+), 922 deletions(-) create mode 100644 cmd/kratos-admin/main.go create mode 100644 cmd/kratos-admin/wire.go create mode 100644 cmd/kratos-admin/wire_gen.go create mode 100644 internal/data/config_store.go create mode 100644 internal/data/database.go create mode 100644 internal/data/qiniu_storage.go create mode 100644 internal/data/s3_storage.go delete mode 100644 web/src/components/charts/index.vue delete mode 100644 web/src/view/dashboard/components/charts-content-numbers.vue delete mode 100644 web/src/view/dashboard/components/charts-people-numbers.vue delete mode 100644 web/src/view/dashboard/components/charts.vue diff --git a/.gitignore b/.gitignore index 74f750b..86a567c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ Thumbs.db *.key *.log bin/ -/cmd/kratos-admin +/cmd/kratos-admin/kratos-admin # Develop tools .vscode/ @@ -41,3 +41,4 @@ bin/ web/dist/ web/node_modules/ +.pnpm-store/ diff --git a/cmd/kratos-admin/main.go b/cmd/kratos-admin/main.go new file mode 100644 index 0000000..4032052 --- /dev/null +++ b/cmd/kratos-admin/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "flag" + "log/slog" + "os" + "path/filepath" + + "kra/internal/conf" + "kra/internal/server" + + "github.com/go-kratos/kratos/contrib/otel/v3/tracing" + "github.com/go-kratos/kratos/v3" + "github.com/go-kratos/kratos/v3/config" + "github.com/go-kratos/kratos/v3/config/file" + "github.com/go-kratos/kratos/v3/log" + "github.com/go-kratos/kratos/v3/transport/grpc" + + _ "go.uber.org/automaxprocs" +) + +// go build -ldflags "-X main.Version=x.y.z" +var ( + // Name is the name of the compiled software. + Name string + // Version is the version of the compiled software. + Version string + // flagconf is the config flag. + flagconf string + + id, _ = os.Hostname() +) + +func init() { + flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml") +} + +func newApp(logger *slog.Logger, gs *grpc.Server, hs *server.GinServer, scheduler *server.TaskScheduler) *kratos.App { + return kratos.New( + kratos.ID(id), + kratos.Name(Name), + kratos.Version(Version), + kratos.Metadata(map[string]string{}), + kratos.Logger(logger), + kratos.Server( + gs, + hs, + scheduler, + ), + ) +} + +func main() { + flag.Parse() + logger := log.NewLogger( + slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelInfo, + }), + log.WithExtractor(tracing.TraceAttrs), + ).With( + slog.String("service.id", id), + slog.String("service.name", Name), + slog.String("service.version", Version), + ) + log.SetDefault(logger) + c := config.New( + config.WithSource( + file.NewSource(flagconf), + ), + ) + defer c.Close() + + if err := c.Load(); err != nil { + panic(err) + } + + var bc conf.Bootstrap + if err := c.Scan(&bc); err != nil { + panic(err) + } + if bc.Admin != nil { + bc.Admin.ConfigPath = flagconf + if info, err := os.Stat(flagconf); err == nil && info.IsDir() { + bc.Admin.ConfigPath = filepath.Join(flagconf, "config.yaml") + } + } + + app, cleanup, err := wireApp(bc.Server, bc.Data, bc.Admin, logger) + if err != nil { + panic(err) + } + defer cleanup() + + // start and wait for stop signal + if err := app.Run(); err != nil { + panic(err) + } +} diff --git a/cmd/kratos-admin/wire.go b/cmd/kratos-admin/wire.go new file mode 100644 index 0000000..f113d21 --- /dev/null +++ b/cmd/kratos-admin/wire.go @@ -0,0 +1,24 @@ +//go:build wireinject +// +build wireinject + +// The build tag makes sure the stub is not built in the final build. + +package main + +import ( + "log/slog" + + "kra/internal/biz" + "kra/internal/conf" + "kra/internal/data" + "kra/internal/server" + "kra/internal/service" + + "github.com/go-kratos/kratos/v3" + "github.com/google/wire" +) + +// wireApp init kratos application. +func wireApp(*conf.Server, *conf.Data, *conf.AdminBackend, *slog.Logger) (*kratos.App, func(), error) { + panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp)) +} diff --git a/cmd/kratos-admin/wire_gen.go b/cmd/kratos-admin/wire_gen.go new file mode 100644 index 0000000..5c1a71a --- /dev/null +++ b/cmd/kratos-admin/wire_gen.go @@ -0,0 +1,77 @@ +// 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/go-kratos/kratos/v3" + "kra/internal/biz" + "kra/internal/conf" + "kra/internal/data" + "kra/internal/server" + "kra/internal/service" + "log/slog" +) + +import ( + _ "go.uber.org/automaxprocs" +) + +// Injectors from wire.go: + +// wireApp init kratos application. +func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.AdminBackend, logger *slog.Logger) (*kratos.App, func(), error) { + dataData, cleanup, err := data.NewData(confData, adminBackend) + if err != nil { + return nil, nil, err + } + adminRepo := data.NewAdminRepo(dataData) + adminUsecase := biz.NewAdminUsecase(adminRepo) + adminService := service.NewAdminService(adminUsecase) + grpcServer := server.NewGRPCServer(confServer, adminService) + systemRepo := data.NewSystemRepo(dataData) + cache := data.NewCache(dataData) + fileStorage, err := data.NewFileStorage(adminBackend) + if err != nil { + cleanup() + return nil, nil, err + } + systemUsecase := biz.NewSystemUsecase(systemRepo, cache, fileStorage) + settingsRepo := data.NewSettingsRepo(dataData) + settingsUsecase := biz.NewSettingsUsecase(settingsRepo) + settingsService := service.NewSettingsService(settingsUsecase, adminBackend) + systemService := service.NewSystemService(systemUsecase, adminBackend, settingsService) + accessRepo := data.NewAccessRepo(dataData) + accessUsecase := biz.NewAccessUsecase(accessRepo) + accessService := service.NewAccessService(accessUsecase) + versionRepo := data.NewVersionRepo(dataData) + versionUsecase := biz.NewVersionUsecase(versionRepo) + versionService := service.NewVersionService(versionUsecase) + exportRepo := data.NewExportRepo(dataData) + exportUsecase := biz.NewExportUsecase(exportRepo) + exportService := service.NewExportService(exportUsecase) + auditRepo := data.NewAuditRepo(dataData) + auditUsecase := biz.NewAuditUsecase(auditRepo) + auditService := service.NewAuditService(auditUsecase) + taskRepo := data.NewTaskRepo(dataData) + taskUsecase := biz.NewTaskUsecase(taskRepo) + taskService := service.NewTaskService(taskUsecase) + mediaRepo := data.NewMediaRepo(dataData) + mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage) + mediaService := service.NewMediaService(mediaUsecase) + announcementRepo := data.NewAnnouncementRepo(dataData) + announcementUsecase := biz.NewAnnouncementUsecase(announcementRepo) + announcementService := service.NewAnnouncementService(announcementUsecase) + emailRepo := data.NewEmailRepo(adminBackend) + emailUsecase := biz.NewEmailUsecase(emailRepo) + emailService := service.NewEmailService(emailUsecase) + taskScheduler := server.NewTaskScheduler(taskService, logger) + ginServer := server.NewGinServer(confServer, adminBackend, systemService, accessService, settingsService, versionService, exportService, auditService, taskService, mediaService, announcementService, emailService, taskScheduler, logger) + app := newApp(logger, grpcServer, ginServer, taskScheduler) + return app, func() { + cleanup() + }, nil +} diff --git a/configs/config.yaml b/configs/config.yaml index 354db76..23e9322 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -9,6 +9,12 @@ data: database: driver: mysql source: root:root@tcp(127.0.0.1:3306)/test?timeout=5s&parseTime=True&loc=Local&charset=utf8mb4 + host: 127.0.0.1 + port: "3306" + user: root + password: root + name: test + config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4 redis: addr: 127.0.0.1:6379 read_timeout: 0.2s @@ -18,17 +24,21 @@ admin: jwt: # Production deployments must override this value with a private secret. signing_key: change-me-before-production - expires_time: 168h - buffer_time: 24h + expires_time: 604800s + buffer_time: 86400s issuer: kra captcha: key_long: 6 img_width: 240 img_height: 80 - store_expiration: 3m + store_expiration: 180s local: store_path: uploads/file path_prefix: uploads/file + storage: + # local, qiniu, aliyun-oss, huawei-obs, tencent-cos, aws-s3, + # cloudflare-r2 or minio + type: local email: # Leave host/from/secret empty to disable SMTP error notifications. to: "" diff --git a/go.mod b/go.mod index 5bc7898..cc5e208 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,20 @@ go 1.25.7 require ( entgo.io/ent v0.14.6 github.com/casbin/casbin/v3 v3.10.0 + github.com/dzwvip/gorm-oracle v0.1.2 github.com/gin-gonic/gin v1.10.0 - github.com/go-kratos/aip-go/ents v0.0.0-20251213081434-74ffa1fc1588 + github.com/glebarez/sqlite v1.11.0 github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59 github.com/go-kratos/kratos/v3 v3.0.0 - github.com/go-sql-driver/mysql v1.10.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 github.com/google/wire v0.7.0 + github.com/minio/minio-go/v7 v7.0.92 github.com/mojocn/base64Captcha v1.3.8 + github.com/qiniu/go-sdk/v7 v7.25.2 github.com/redis/go-redis/v9 v9.7.0 github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil/v4 v4.25.7 github.com/xuri/excelize/v2 v2.9.0 go.einride.tech/aip v0.86.3 go.uber.org/automaxprocs v1.6.0 @@ -23,7 +26,10 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 + gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.6.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/driver/sqlserver v1.6.3 gorm.io/gorm v1.31.1 ) @@ -32,7 +38,9 @@ replace github.com/go-kratos/kratos/v3 v3.0.0 => github.com/go-kratos/kratos/v3 require ( ariga.io/atlas v1.2.2 // indirect filippo.io/edwards25519 v1.2.0 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect github.com/agext/levenshtein v1.2.3 // indirect + github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect @@ -43,39 +51,69 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/emirpasic/gods v1.12.0 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gammazero/toposort v0.1.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/inflect v0.21.6 // indirect github.com/go-playground/form/v4 v4.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-sql-driver/mysql v1.10.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/hcl/v2 v2.24.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microsoft/go-mssqldb v1.8.2 // indirect + github.com/minio/crc64nvme v1.0.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sijms/go-ora/v2 v2.7.17 // indirect + github.com/thoas/go-funk v0.7.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zclconf/go-cty v1.18.1 // indirect github.com/zclconf/go-cty-yaml v1.2.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -91,5 +129,9 @@ require ( golang.org/x/text v0.38.0 // indirect golang.org/x/tools v0.46.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/fileutil v1.0.0 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) diff --git a/go.sum b/go.sum index f833757..e979a82 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,32 @@ entgo.io/ent v0.14.6 h1:/f2696BpwuWAEEG6PVGWflg6+Inrpq4pRWuNlWz/Skk= entgo.io/ent v0.14.6/go.mod h1:z46QBUdGC+BATwsedbDuREfSS0oSCV+csdEYlL4p73s= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= @@ -36,11 +58,23 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g= +github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= @@ -50,12 +84,18 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= +github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-kratos/aip-go/ents v0.0.0-20251213081434-74ffa1fc1588 h1:e0dWyNWFeTgGCH7cRMROahTwMaQYtmHce/6fxVmA6yI= -github.com/go-kratos/aip-go/ents v0.0.0-20251213081434-74ffa1fc1588/go.mod h1:ifKMm4eJmaQz5WNKKhfClpCng8UxUB3sArof8wjwG78= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59 h1:FeDujUZF6a2pPS0RWGI52IXWPqVcGW1QP5vHDlAqLE8= github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59/go.mod h1:hT2QNZ/0DPOvRqVqxoBBL/aMCOHhbbWh92mF87DBiCU= github.com/go-kratos/kratos/v3 v3.0.0-20260617100506-4e232a3eff59 h1:32laNndL0GsYLuGwWquB6if8yrBQWEzXlkkXImcfeKA= @@ -65,66 +105,128 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/inflect v0.21.6 h1:0Se5BlyDT4hnV9JQQKA63W9TIUU6SC4hPJiY3qJOEzQ= github.com/go-openapi/inflect v0.21.6/go.mod h1:ksYcnLD7j24H79hdqOMmWaLXjFXd0LTkRoBA0UazLW8= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.7.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= +github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.92 h1:jpBFWyRS3p8P/9tsRc+NuvqoFi7qAmTCFPoRFmobbVw= +github.com/minio/minio-go/v7 v7.0.92/go.mod h1:vTIc8DNcnAZIhyFsk8EB90AbPjj3j68aWIEQCiPj7d0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -132,20 +234,37 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= +github.com/qiniu/go-sdk/v7 v7.25.2 h1:URwgZpxySdiwu2yQpHk93X4LXWHyFRp1x3Vmlk/YWvo= +github.com/qiniu/go-sdk/v7 v7.25.2/go.mod h1:dmKtJ2ahhPWFVi9o1D5GemmWoh/ctuB9peqTowyTO8o= +github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= @@ -153,13 +272,25 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 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/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM= +github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U= +github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU= +github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -168,6 +299,14 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y= +github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= @@ -179,6 +318,8 @@ github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmji github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM= github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= @@ -205,29 +346,48 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -236,38 +396,64 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= @@ -280,6 +466,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc= @@ -293,16 +480,39 @@ google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zN google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI= +gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/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= +modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/biz/system.go b/internal/biz/system.go index b56d70e..9595a7e 100644 --- a/internal/biz/system.go +++ b/internal/biz/system.go @@ -35,6 +35,10 @@ type UserListFilter struct { Desc bool } +type DatabaseConfig struct { + Driver, Host, Port, User, Password, Name, Path, Config, AdminPassword string +} + type Authority struct { AuthorityID uint AuthorityName string @@ -64,7 +68,7 @@ type Menu struct { type SystemRepo interface { IsInitialized(context.Context) (bool, error) - Initialize(context.Context) error + Initialize(context.Context, *DatabaseConfig) error FindUserByUsername(context.Context, string) (*User, error) FindUserByID(context.Context, uint) (*User, error) MenusByAuthority(context.Context, uint) ([]*Menu, error) @@ -72,11 +76,12 @@ type SystemRepo interface { CreateUser(context.Context, *User) (*User, error) UpdateUser(context.Context, *User) error DeleteUser(context.Context, uint) error - UpdatePassword(context.Context, uint, string) error + UpdatePassword(context.Context, uint, string, bool) error SetUserAuthorities(context.Context, uint, []uint) error SetUserAuthority(context.Context, uint, uint) error SetUserSetting(context.Context, uint, map[string]any) error ListAuthorities(context.Context) ([]*Authority, error) + PersistConfig(context.Context) error } type SystemUsecase struct { @@ -102,7 +107,13 @@ func (uc *SystemUsecase) CacheDelete(ctx context.Context, key string) error { func (uc *SystemUsecase) IsInitialized(ctx context.Context) (bool, error) { return uc.repo.IsInitialized(ctx) } -func (uc *SystemUsecase) Initialize(ctx context.Context) error { return uc.repo.Initialize(ctx) } +func (uc *SystemUsecase) Initialize(ctx context.Context, config *DatabaseConfig) error { + if config == nil || len(config.AdminPassword) < 6 { + return ErrInvalidCredentials + } + return uc.repo.Initialize(ctx, config) +} +func (uc *SystemUsecase) PersistConfig(ctx context.Context) error { return uc.repo.PersistConfig(ctx) } func (uc *SystemUsecase) Login(ctx context.Context, username, password string) (*User, error) { u, err := uc.repo.FindUserByUsername(ctx, username) @@ -142,7 +153,7 @@ func (uc *SystemUsecase) ResetPassword(ctx context.Context, id uint, password st if err != nil { return err } - return uc.repo.UpdatePassword(ctx, id, string(hash)) + return uc.repo.UpdatePassword(ctx, id, string(hash), false) } func (uc *SystemUsecase) ChangePassword(ctx context.Context, id uint, oldPassword, newPassword string) error { user, err := uc.repo.FindUserByID(ctx, id) @@ -152,7 +163,11 @@ func (uc *SystemUsecase) ChangePassword(ctx context.Context, id uint, oldPasswor if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)) != nil { return ErrInvalidCredentials } - return uc.ResetPassword(ctx, id, newPassword) + hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + return uc.repo.UpdatePassword(ctx, id, string(hash), true) } func (uc *SystemUsecase) Authorities(ctx context.Context) ([]*Authority, error) { return uc.repo.ListAuthorities(ctx) diff --git a/internal/conf/conf.pb.go b/internal/conf/conf.pb.go index 127b003..7602f6b 100644 --- a/internal/conf/conf.pb.go +++ b/internal/conf/conf.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.35.1 +// protoc (unknown) // source: conf/conf.proto package conf @@ -188,12 +188,15 @@ func (x *Data) GetRedis() *Data_Redis { // AdminBackend contains settings for the administration HTTP transport. type AdminBackend struct { - state protoimpl.MessageState `protogen:"open.v1"` - RouterPrefix string `protobuf:"bytes,1,opt,name=router_prefix,json=routerPrefix,proto3" json:"router_prefix,omitempty"` - Jwt *AdminBackend_JWT `protobuf:"bytes,2,opt,name=jwt,proto3" json:"jwt,omitempty"` - Captcha *AdminBackend_Captcha `protobuf:"bytes,3,opt,name=captcha,proto3" json:"captcha,omitempty"` - Local *AdminBackend_Local `protobuf:"bytes,4,opt,name=local,proto3" json:"local,omitempty"` - Email *AdminBackend_Email `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RouterPrefix string `protobuf:"bytes,1,opt,name=router_prefix,json=routerPrefix,proto3" json:"router_prefix,omitempty"` + Jwt *AdminBackend_JWT `protobuf:"bytes,2,opt,name=jwt,proto3" json:"jwt,omitempty"` + Captcha *AdminBackend_Captcha `protobuf:"bytes,3,opt,name=captcha,proto3" json:"captcha,omitempty"` + Local *AdminBackend_Local `protobuf:"bytes,4,opt,name=local,proto3" json:"local,omitempty"` + Email *AdminBackend_Email `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"` + Storage *AdminBackend_Storage `protobuf:"bytes,6,opt,name=storage,proto3" json:"storage,omitempty"` + // ConfigPath is populated by the entrypoint and is not required in YAML. + ConfigPath string `protobuf:"bytes,7,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -263,6 +266,20 @@ func (x *AdminBackend) GetEmail() *AdminBackend_Email { return nil } +func (x *AdminBackend) GetStorage() *AdminBackend_Storage { + if x != nil { + return x.Storage + } + return nil +} + +func (x *AdminBackend) GetConfigPath() string { + if x != nil { + return x.ConfigPath + } + return "" +} + type Server_HTTP struct { state protoimpl.MessageState `protogen:"open.v1"` Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"` @@ -387,6 +404,13 @@ type Data_Database struct { state protoimpl.MessageState `protogen:"open.v1"` Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"` Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` + Host string `protobuf:"bytes,3,opt,name=host,proto3" json:"host,omitempty"` + Port string `protobuf:"bytes,4,opt,name=port,proto3" json:"port,omitempty"` + User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"` + Password string `protobuf:"bytes,6,opt,name=password,proto3" json:"password,omitempty"` + Name string `protobuf:"bytes,7,opt,name=name,proto3" json:"name,omitempty"` + Config string `protobuf:"bytes,8,opt,name=config,proto3" json:"config,omitempty"` + Path string `protobuf:"bytes,9,opt,name=path,proto3" json:"path,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -435,6 +459,55 @@ func (x *Data_Database) GetSource() string { return "" } +func (x *Data_Database) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *Data_Database) GetPort() string { + if x != nil { + return x.Port + } + return "" +} + +func (x *Data_Database) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *Data_Database) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *Data_Database) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Data_Database) GetConfig() string { + if x != nil { + return x.Config + } + return "" +} + +func (x *Data_Database) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + type Data_Redis struct { state protoimpl.MessageState `protogen:"open.v1"` Network string `protobuf:"bytes,1,opt,name=network,proto3" json:"network,omitempty"` @@ -791,6 +864,316 @@ func (x *AdminBackend_Email) GetIsLoginAuth() bool { return false } +type AdminBackend_Storage struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Qiniu *AdminBackend_Qiniu `protobuf:"bytes,2,opt,name=qiniu,proto3" json:"qiniu,omitempty"` + AliyunOss *AdminBackend_ObjectStore `protobuf:"bytes,3,opt,name=aliyun_oss,json=aliyunOss,proto3" json:"aliyun_oss,omitempty"` + HuaweiObs *AdminBackend_ObjectStore `protobuf:"bytes,4,opt,name=huawei_obs,json=huaweiObs,proto3" json:"huawei_obs,omitempty"` + TencentCos *AdminBackend_ObjectStore `protobuf:"bytes,5,opt,name=tencent_cos,json=tencentCos,proto3" json:"tencent_cos,omitempty"` + AwsS3 *AdminBackend_ObjectStore `protobuf:"bytes,6,opt,name=aws_s3,json=awsS3,proto3" json:"aws_s3,omitempty"` + CloudflareR2 *AdminBackend_ObjectStore `protobuf:"bytes,7,opt,name=cloudflare_r2,json=cloudflareR2,proto3" json:"cloudflare_r2,omitempty"` + Minio *AdminBackend_ObjectStore `protobuf:"bytes,8,opt,name=minio,proto3" json:"minio,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminBackend_Storage) Reset() { + *x = AdminBackend_Storage{} + mi := &file_conf_conf_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminBackend_Storage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminBackend_Storage) ProtoMessage() {} + +func (x *AdminBackend_Storage) ProtoReflect() protoreflect.Message { + mi := &file_conf_conf_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminBackend_Storage.ProtoReflect.Descriptor instead. +func (*AdminBackend_Storage) Descriptor() ([]byte, []int) { + return file_conf_conf_proto_rawDescGZIP(), []int{3, 4} +} + +func (x *AdminBackend_Storage) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *AdminBackend_Storage) GetQiniu() *AdminBackend_Qiniu { + if x != nil { + return x.Qiniu + } + return nil +} + +func (x *AdminBackend_Storage) GetAliyunOss() *AdminBackend_ObjectStore { + if x != nil { + return x.AliyunOss + } + return nil +} + +func (x *AdminBackend_Storage) GetHuaweiObs() *AdminBackend_ObjectStore { + if x != nil { + return x.HuaweiObs + } + return nil +} + +func (x *AdminBackend_Storage) GetTencentCos() *AdminBackend_ObjectStore { + if x != nil { + return x.TencentCos + } + return nil +} + +func (x *AdminBackend_Storage) GetAwsS3() *AdminBackend_ObjectStore { + if x != nil { + return x.AwsS3 + } + return nil +} + +func (x *AdminBackend_Storage) GetCloudflareR2() *AdminBackend_ObjectStore { + if x != nil { + return x.CloudflareR2 + } + return nil +} + +func (x *AdminBackend_Storage) GetMinio() *AdminBackend_ObjectStore { + if x != nil { + return x.Minio + } + return nil +} + +type AdminBackend_Qiniu struct { + state protoimpl.MessageState `protogen:"open.v1"` + Zone string `protobuf:"bytes,1,opt,name=zone,proto3" json:"zone,omitempty"` + Bucket string `protobuf:"bytes,2,opt,name=bucket,proto3" json:"bucket,omitempty"` + BaseUrl string `protobuf:"bytes,3,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + AccessKey string `protobuf:"bytes,4,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` + SecretKey string `protobuf:"bytes,5,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"` + UseHttps bool `protobuf:"varint,6,opt,name=use_https,json=useHttps,proto3" json:"use_https,omitempty"` + UseCdnDomains bool `protobuf:"varint,7,opt,name=use_cdn_domains,json=useCdnDomains,proto3" json:"use_cdn_domains,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminBackend_Qiniu) Reset() { + *x = AdminBackend_Qiniu{} + mi := &file_conf_conf_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminBackend_Qiniu) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminBackend_Qiniu) ProtoMessage() {} + +func (x *AdminBackend_Qiniu) ProtoReflect() protoreflect.Message { + mi := &file_conf_conf_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminBackend_Qiniu.ProtoReflect.Descriptor instead. +func (*AdminBackend_Qiniu) Descriptor() ([]byte, []int) { + return file_conf_conf_proto_rawDescGZIP(), []int{3, 5} +} + +func (x *AdminBackend_Qiniu) GetZone() string { + if x != nil { + return x.Zone + } + return "" +} + +func (x *AdminBackend_Qiniu) GetBucket() string { + if x != nil { + return x.Bucket + } + return "" +} + +func (x *AdminBackend_Qiniu) GetBaseUrl() string { + if x != nil { + return x.BaseUrl + } + return "" +} + +func (x *AdminBackend_Qiniu) GetAccessKey() string { + if x != nil { + return x.AccessKey + } + return "" +} + +func (x *AdminBackend_Qiniu) GetSecretKey() string { + if x != nil { + return x.SecretKey + } + return "" +} + +func (x *AdminBackend_Qiniu) GetUseHttps() bool { + if x != nil { + return x.UseHttps + } + return false +} + +func (x *AdminBackend_Qiniu) GetUseCdnDomains() bool { + if x != nil { + return x.UseCdnDomains + } + return false +} + +// ObjectStore covers S3-compatible configuration used by public cloud +// providers and self-hosted MinIO. +type AdminBackend_ObjectStore struct { + state protoimpl.MessageState `protogen:"open.v1"` + Endpoint string `protobuf:"bytes,1,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + Region string `protobuf:"bytes,2,opt,name=region,proto3" json:"region,omitempty"` + Bucket string `protobuf:"bytes,3,opt,name=bucket,proto3" json:"bucket,omitempty"` + AccessKey string `protobuf:"bytes,4,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` + SecretKey string `protobuf:"bytes,5,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"` + BaseUrl string `protobuf:"bytes,6,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + PathPrefix string `protobuf:"bytes,7,opt,name=path_prefix,json=pathPrefix,proto3" json:"path_prefix,omitempty"` + UseSsl bool `protobuf:"varint,8,opt,name=use_ssl,json=useSsl,proto3" json:"use_ssl,omitempty"` + ForcePathStyle bool `protobuf:"varint,9,opt,name=force_path_style,json=forcePathStyle,proto3" json:"force_path_style,omitempty"` + AccountId string `protobuf:"bytes,10,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminBackend_ObjectStore) Reset() { + *x = AdminBackend_ObjectStore{} + mi := &file_conf_conf_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminBackend_ObjectStore) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminBackend_ObjectStore) ProtoMessage() {} + +func (x *AdminBackend_ObjectStore) ProtoReflect() protoreflect.Message { + mi := &file_conf_conf_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminBackend_ObjectStore.ProtoReflect.Descriptor instead. +func (*AdminBackend_ObjectStore) Descriptor() ([]byte, []int) { + return file_conf_conf_proto_rawDescGZIP(), []int{3, 6} +} + +func (x *AdminBackend_ObjectStore) GetEndpoint() string { + if x != nil { + return x.Endpoint + } + return "" +} + +func (x *AdminBackend_ObjectStore) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +func (x *AdminBackend_ObjectStore) GetBucket() string { + if x != nil { + return x.Bucket + } + return "" +} + +func (x *AdminBackend_ObjectStore) GetAccessKey() string { + if x != nil { + return x.AccessKey + } + return "" +} + +func (x *AdminBackend_ObjectStore) GetSecretKey() string { + if x != nil { + return x.SecretKey + } + return "" +} + +func (x *AdminBackend_ObjectStore) GetBaseUrl() string { + if x != nil { + return x.BaseUrl + } + return "" +} + +func (x *AdminBackend_ObjectStore) GetPathPrefix() string { + if x != nil { + return x.PathPrefix + } + return "" +} + +func (x *AdminBackend_ObjectStore) GetUseSsl() bool { + if x != nil { + return x.UseSsl + } + return false +} + +func (x *AdminBackend_ObjectStore) GetForcePathStyle() bool { + if x != nil { + return x.ForcePathStyle + } + return false +} + +func (x *AdminBackend_ObjectStore) GetAccountId() string { + if x != nil { + return x.AccountId + } + return "" +} + var File_conf_conf_proto protoreflect.FileDescriptor const file_conf_conf_proto_rawDesc = "" + @@ -811,24 +1194,34 @@ const file_conf_conf_proto_rawDesc = "" + "\x04GRPC\x12\x18\n" + "\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" + "\x04addr\x18\x02 \x01(\tR\x04addr\x123\n" + - "\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xdd\x02\n" + + "\atimeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\atimeout\"\xf6\x03\n" + "\x04Data\x125\n" + "\bdatabase\x18\x01 \x01(\v2\x19.kratos.api.Data.DatabaseR\bdatabase\x12,\n" + - "\x05redis\x18\x02 \x01(\v2\x16.kratos.api.Data.RedisR\x05redis\x1a:\n" + + "\x05redis\x18\x02 \x01(\v2\x16.kratos.api.Data.RedisR\x05redis\x1a\xd2\x01\n" + "\bDatabase\x12\x16\n" + "\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" + - "\x06source\x18\x02 \x01(\tR\x06source\x1a\xb3\x01\n" + + "\x06source\x18\x02 \x01(\tR\x06source\x12\x12\n" + + "\x04host\x18\x03 \x01(\tR\x04host\x12\x12\n" + + "\x04port\x18\x04 \x01(\tR\x04port\x12\x12\n" + + "\x04user\x18\x05 \x01(\tR\x04user\x12\x1a\n" + + "\bpassword\x18\x06 \x01(\tR\bpassword\x12\x12\n" + + "\x04name\x18\a \x01(\tR\x04name\x12\x16\n" + + "\x06config\x18\b \x01(\tR\x06config\x12\x12\n" + + "\x04path\x18\t \x01(\tR\x04path\x1a\xb3\x01\n" + "\x05Redis\x12\x18\n" + "\anetwork\x18\x01 \x01(\tR\anetwork\x12\x12\n" + "\x04addr\x18\x02 \x01(\tR\x04addr\x12<\n" + "\fread_timeout\x18\x03 \x01(\v2\x19.google.protobuf.DurationR\vreadTimeout\x12>\n" + - "\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\"\xfd\x06\n" + + "\rwrite_timeout\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\fwriteTimeout\"\xd1\x0f\n" + "\fAdminBackend\x12#\n" + "\rrouter_prefix\x18\x01 \x01(\tR\frouterPrefix\x12.\n" + "\x03jwt\x18\x02 \x01(\v2\x1c.kratos.api.AdminBackend.JWTR\x03jwt\x12:\n" + "\acaptcha\x18\x03 \x01(\v2 .kratos.api.AdminBackend.CaptchaR\acaptcha\x124\n" + "\x05local\x18\x04 \x01(\v2\x1e.kratos.api.AdminBackend.LocalR\x05local\x124\n" + - "\x05email\x18\x05 \x01(\v2\x1e.kratos.api.AdminBackend.EmailR\x05email\x1a\xb8\x01\n" + + "\x05email\x18\x05 \x01(\v2\x1e.kratos.api.AdminBackend.EmailR\x05email\x12:\n" + + "\astorage\x18\x06 \x01(\v2 .kratos.api.AdminBackend.StorageR\astorage\x12\x1f\n" + + "\vconfig_path\x18\a \x01(\tR\n" + + "configPath\x1a\xb8\x01\n" + "\x03JWT\x12\x1f\n" + "\vsigning_key\x18\x01 \x01(\tR\n" + "signingKey\x12<\n" + @@ -855,7 +1248,45 @@ const file_conf_conf_proto_rawDesc = "" + "\bnickname\x18\x05 \x01(\tR\bnickname\x12\x12\n" + "\x04port\x18\x06 \x01(\x05R\x04port\x12\x15\n" + "\x06is_ssl\x18\a \x01(\bR\x05isSsl\x12\"\n" + - "\ris_login_auth\x18\b \x01(\bR\visLoginAuthB\x18Z\x16kra/internal/conf;confb\x06proto3" + "\ris_login_auth\x18\b \x01(\bR\visLoginAuth\x1a\xe8\x03\n" + + "\aStorage\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x124\n" + + "\x05qiniu\x18\x02 \x01(\v2\x1e.kratos.api.AdminBackend.QiniuR\x05qiniu\x12C\n" + + "\n" + + "aliyun_oss\x18\x03 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\taliyunOss\x12C\n" + + "\n" + + "huawei_obs\x18\x04 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\thuaweiObs\x12E\n" + + "\vtencent_cos\x18\x05 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\n" + + "tencentCos\x12;\n" + + "\x06aws_s3\x18\x06 \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\x05awsS3\x12I\n" + + "\rcloudflare_r2\x18\a \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\fcloudflareR2\x12:\n" + + "\x05minio\x18\b \x01(\v2$.kratos.api.AdminBackend.ObjectStoreR\x05minio\x1a\xd1\x01\n" + + "\x05Qiniu\x12\x12\n" + + "\x04zone\x18\x01 \x01(\tR\x04zone\x12\x16\n" + + "\x06bucket\x18\x02 \x01(\tR\x06bucket\x12\x19\n" + + "\bbase_url\x18\x03 \x01(\tR\abaseUrl\x12\x1d\n" + + "\n" + + "access_key\x18\x04 \x01(\tR\taccessKey\x12\x1d\n" + + "\n" + + "secret_key\x18\x05 \x01(\tR\tsecretKey\x12\x1b\n" + + "\tuse_https\x18\x06 \x01(\bR\buseHttps\x12&\n" + + "\x0fuse_cdn_domains\x18\a \x01(\bR\ruseCdnDomains\x1a\xb5\x02\n" + + "\vObjectStore\x12\x1a\n" + + "\bendpoint\x18\x01 \x01(\tR\bendpoint\x12\x16\n" + + "\x06region\x18\x02 \x01(\tR\x06region\x12\x16\n" + + "\x06bucket\x18\x03 \x01(\tR\x06bucket\x12\x1d\n" + + "\n" + + "access_key\x18\x04 \x01(\tR\taccessKey\x12\x1d\n" + + "\n" + + "secret_key\x18\x05 \x01(\tR\tsecretKey\x12\x19\n" + + "\bbase_url\x18\x06 \x01(\tR\abaseUrl\x12\x1f\n" + + "\vpath_prefix\x18\a \x01(\tR\n" + + "pathPrefix\x12\x17\n" + + "\ause_ssl\x18\b \x01(\bR\x06useSsl\x12(\n" + + "\x10force_path_style\x18\t \x01(\bR\x0eforcePathStyle\x12\x1d\n" + + "\n" + + "account_id\x18\n" + + " \x01(\tR\taccountIdB\x18Z\x16kra/internal/conf;confb\x06proto3" var ( file_conf_conf_proto_rawDescOnce sync.Once @@ -869,21 +1300,24 @@ func file_conf_conf_proto_rawDescGZIP() []byte { return file_conf_conf_proto_rawDescData } -var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 15) var file_conf_conf_proto_goTypes = []any{ - (*Bootstrap)(nil), // 0: kratos.api.Bootstrap - (*Server)(nil), // 1: kratos.api.Server - (*Data)(nil), // 2: kratos.api.Data - (*AdminBackend)(nil), // 3: kratos.api.AdminBackend - (*Server_HTTP)(nil), // 4: kratos.api.Server.HTTP - (*Server_GRPC)(nil), // 5: kratos.api.Server.GRPC - (*Data_Database)(nil), // 6: kratos.api.Data.Database - (*Data_Redis)(nil), // 7: kratos.api.Data.Redis - (*AdminBackend_JWT)(nil), // 8: kratos.api.AdminBackend.JWT - (*AdminBackend_Captcha)(nil), // 9: kratos.api.AdminBackend.Captcha - (*AdminBackend_Local)(nil), // 10: kratos.api.AdminBackend.Local - (*AdminBackend_Email)(nil), // 11: kratos.api.AdminBackend.Email - (*durationpb.Duration)(nil), // 12: google.protobuf.Duration + (*Bootstrap)(nil), // 0: kratos.api.Bootstrap + (*Server)(nil), // 1: kratos.api.Server + (*Data)(nil), // 2: kratos.api.Data + (*AdminBackend)(nil), // 3: kratos.api.AdminBackend + (*Server_HTTP)(nil), // 4: kratos.api.Server.HTTP + (*Server_GRPC)(nil), // 5: kratos.api.Server.GRPC + (*Data_Database)(nil), // 6: kratos.api.Data.Database + (*Data_Redis)(nil), // 7: kratos.api.Data.Redis + (*AdminBackend_JWT)(nil), // 8: kratos.api.AdminBackend.JWT + (*AdminBackend_Captcha)(nil), // 9: kratos.api.AdminBackend.Captcha + (*AdminBackend_Local)(nil), // 10: kratos.api.AdminBackend.Local + (*AdminBackend_Email)(nil), // 11: kratos.api.AdminBackend.Email + (*AdminBackend_Storage)(nil), // 12: kratos.api.AdminBackend.Storage + (*AdminBackend_Qiniu)(nil), // 13: kratos.api.AdminBackend.Qiniu + (*AdminBackend_ObjectStore)(nil), // 14: kratos.api.AdminBackend.ObjectStore + (*durationpb.Duration)(nil), // 15: google.protobuf.Duration } var file_conf_conf_proto_depIdxs = []int32{ 1, // 0: kratos.api.Bootstrap.server:type_name -> kratos.api.Server @@ -897,18 +1331,26 @@ var file_conf_conf_proto_depIdxs = []int32{ 9, // 8: kratos.api.AdminBackend.captcha:type_name -> kratos.api.AdminBackend.Captcha 10, // 9: kratos.api.AdminBackend.local:type_name -> kratos.api.AdminBackend.Local 11, // 10: kratos.api.AdminBackend.email:type_name -> kratos.api.AdminBackend.Email - 12, // 11: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration - 12, // 12: kratos.api.Server.GRPC.timeout:type_name -> google.protobuf.Duration - 12, // 13: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration - 12, // 14: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration - 12, // 15: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration - 12, // 16: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration - 12, // 17: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration - 18, // [18:18] is the sub-list for method output_type - 18, // [18:18] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 12, // 11: kratos.api.AdminBackend.storage:type_name -> kratos.api.AdminBackend.Storage + 15, // 12: kratos.api.Server.HTTP.timeout:type_name -> google.protobuf.Duration + 15, // 13: kratos.api.Server.GRPC.timeout:type_name -> google.protobuf.Duration + 15, // 14: kratos.api.Data.Redis.read_timeout:type_name -> google.protobuf.Duration + 15, // 15: kratos.api.Data.Redis.write_timeout:type_name -> google.protobuf.Duration + 15, // 16: kratos.api.AdminBackend.JWT.expires_time:type_name -> google.protobuf.Duration + 15, // 17: kratos.api.AdminBackend.JWT.buffer_time:type_name -> google.protobuf.Duration + 15, // 18: kratos.api.AdminBackend.Captcha.store_expiration:type_name -> google.protobuf.Duration + 13, // 19: kratos.api.AdminBackend.Storage.qiniu:type_name -> kratos.api.AdminBackend.Qiniu + 14, // 20: kratos.api.AdminBackend.Storage.aliyun_oss:type_name -> kratos.api.AdminBackend.ObjectStore + 14, // 21: kratos.api.AdminBackend.Storage.huawei_obs:type_name -> kratos.api.AdminBackend.ObjectStore + 14, // 22: kratos.api.AdminBackend.Storage.tencent_cos:type_name -> kratos.api.AdminBackend.ObjectStore + 14, // 23: kratos.api.AdminBackend.Storage.aws_s3:type_name -> kratos.api.AdminBackend.ObjectStore + 14, // 24: kratos.api.AdminBackend.Storage.cloudflare_r2:type_name -> kratos.api.AdminBackend.ObjectStore + 14, // 25: kratos.api.AdminBackend.Storage.minio:type_name -> kratos.api.AdminBackend.ObjectStore + 26, // [26:26] is the sub-list for method output_type + 26, // [26:26] is the sub-list for method input_type + 26, // [26:26] is the sub-list for extension type_name + 26, // [26:26] is the sub-list for extension extendee + 0, // [0:26] is the sub-list for field type_name } func init() { file_conf_conf_proto_init() } @@ -922,7 +1364,7 @@ func file_conf_conf_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_conf_conf_proto_rawDesc), len(file_conf_conf_proto_rawDesc)), NumEnums: 0, - NumMessages: 12, + NumMessages: 15, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/conf/conf.proto b/internal/conf/conf.proto index d84ea6e..1a999c3 100644 --- a/internal/conf/conf.proto +++ b/internal/conf/conf.proto @@ -30,6 +30,13 @@ message Data { message Database { string driver = 1; string source = 2; + string host = 3; + string port = 4; + string user = 5; + string password = 6; + string name = 7; + string config = 8; + string path = 9; } message Redis { string network = 1; @@ -48,6 +55,9 @@ message AdminBackend { Captcha captcha = 3; Local local = 4; Email email = 5; + Storage storage = 6; + // ConfigPath is populated by the entrypoint and is not required in YAML. + string config_path = 7; message JWT { string signing_key = 1; @@ -78,4 +88,40 @@ message AdminBackend { bool is_ssl = 7; bool is_login_auth = 8; } + + message Storage { + string type = 1; + Qiniu qiniu = 2; + ObjectStore aliyun_oss = 3; + ObjectStore huawei_obs = 4; + ObjectStore tencent_cos = 5; + ObjectStore aws_s3 = 6; + ObjectStore cloudflare_r2 = 7; + ObjectStore minio = 8; + } + + message Qiniu { + string zone = 1; + string bucket = 2; + string base_url = 3; + string access_key = 4; + string secret_key = 5; + bool use_https = 6; + bool use_cdn_domains = 7; + } + + // ObjectStore covers S3-compatible configuration used by public cloud + // providers and self-hosted MinIO. + message ObjectStore { + string endpoint = 1; + string region = 2; + string bucket = 3; + string access_key = 4; + string secret_key = 5; + string base_url = 6; + string path_prefix = 7; + bool use_ssl = 8; + bool force_path_style = 9; + string account_id = 10; + } } diff --git a/internal/data/admin.go b/internal/data/admin.go index b555f85..fda25cf 100644 --- a/internal/data/admin.go +++ b/internal/data/admin.go @@ -2,126 +2,84 @@ package data import ( "context" + "errors" "time" - "github.com/go-kratos/aip-go/ents" + "gorm.io/gorm" "kra/internal/biz" - "kra/internal/data/ent" - "kra/internal/data/ent/admin" ) -func convertAdmin(po *ent.Admin) *biz.Admin { - return &biz.Admin{ - ID: po.ID, - Name: po.Name, - Email: po.Email, - Avatar: po.Avatar, - Access: po.Access, - Password: po.Password, - CreateTime: po.CreateTime, - UpdateTime: po.UpdateTime, +type adminPO struct { + ID int64 `gorm:"primaryKey;autoIncrement"` + Name string `gorm:"uniqueIndex"` + Email string `gorm:"uniqueIndex"` + Password string + Access string + Avatar string + CreateTime time.Time + UpdateTime time.Time +} + +func (adminPO) TableName() string { return "admins" } +func convertAdmin(po *adminPO) *biz.Admin { + return &biz.Admin{ID: po.ID, Name: po.Name, Email: po.Email, Password: po.Password, Access: po.Access, Avatar: po.Avatar, CreateTime: po.CreateTime, UpdateTime: po.UpdateTime} +} + +type adminRepo struct{ data *Data } + +func NewAdminRepo(data *Data) biz.AdminRepo { return &adminRepo{data: data} } +func (r *adminRepo) one(ctx context.Context, query *gorm.DB) (*biz.Admin, error) { + var po adminPO + if err := query.WithContext(ctx).First(&po).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, biz.ErrAdminNotFound + } + return nil, err } + return convertAdmin(&po), nil } - -type adminRepo struct { - data *Data -} - -// NewAdminRepo creates a new AdminRepo instance. -func NewAdminRepo(data *Data) biz.AdminRepo { - return &adminRepo{ - data: data, - } -} - func (r *adminRepo) FindByID(ctx context.Context, id int64) (*biz.Admin, error) { - po, err := r.data.db.Admin.Get(ctx, id) - if err != nil { - if ent.IsNotFound(err) { - return nil, biz.ErrAdminNotFound - } - return nil, err - } - return convertAdmin(po), nil + return r.one(ctx, r.data.gormDB.Where("id = ?", id)) } - func (r *adminRepo) FindByName(ctx context.Context, name string) (*biz.Admin, error) { - po, err := r.data.db.Admin.Query().Where(admin.NameEQ(name)).Only(ctx) - if err != nil { - if ent.IsNotFound(err) { - return nil, biz.ErrAdminNotFound - } - return nil, err - } - return convertAdmin(po), nil + return r.one(ctx, r.data.gormDB.Where("name = ?", name)) } - func (r *adminRepo) FindByEmail(ctx context.Context, email string) (*biz.Admin, error) { - po, err := r.data.db.Admin.Query().Where(admin.EmailEQ(email)).Only(ctx) - if err != nil { - if ent.IsNotFound(err) { - return nil, biz.ErrAdminNotFound - } - return nil, err - } - return convertAdmin(po), nil + return r.one(ctx, r.data.gormDB.Where("email = ?", email)) } - func (r *adminRepo) ListAdmins(ctx context.Context, opts ...biz.ListOption) ([]*biz.Admin, error) { o := biz.ListOptions{Limit: 20} for _, opt := range opts { opt(&o) } - pos, err := r.data.db.Admin.Query(). - Where(ents.ApplyFilter(o.Filter)). - Order(ents.ApplyOrderBy(o.OrderBy)). - Offset(o.Offset). - Limit(o.Limit). - All(ctx) - if err != nil { + var pos []adminPO + if err := r.data.gormDB.WithContext(ctx).Offset(o.Offset).Limit(o.Limit).Order("id asc").Find(&pos).Error; err != nil { return nil, err } - var admins []*biz.Admin - for _, po := range pos { - admins = append(admins, convertAdmin(po)) + out := make([]*biz.Admin, 0, len(pos)) + for i := range pos { + out = append(out, convertAdmin(&pos[i])) } - return admins, nil + return out, nil } - -func (r *adminRepo) CreateAdmin(ctx context.Context, admin *biz.Admin) (*biz.Admin, error) { - po, err := r.data.db.Admin.Create(). - SetName(admin.Name). - SetEmail(admin.Email). - SetAvatar(admin.Avatar). - SetAccess(admin.Access). - SetPassword(admin.Password). - SetCreateTime(time.Now()). - SetUpdateTime(time.Now()). - Save(ctx) - if err != nil { +func (r *adminRepo) CreateAdmin(ctx context.Context, a *biz.Admin) (*biz.Admin, error) { + now := time.Now() + po := adminPO{Name: a.Name, Email: a.Email, Password: a.Password, Access: a.Access, Avatar: a.Avatar, CreateTime: now, UpdateTime: now} + if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { return nil, err } - return convertAdmin(po), nil + return convertAdmin(&po), nil } - -func (r *adminRepo) UpdateAdmin(ctx context.Context, admin *biz.Admin) (*biz.Admin, error) { - update := r.data.db.Admin.UpdateOneID(admin.ID). - SetName(admin.Name). - SetEmail(admin.Email). - SetAccess(admin.Access). - SetAvatar(admin.Avatar). - SetUpdateTime(time.Now()) - // Only update the password if it's not empty - if admin.Password != "" { - update.SetPassword(admin.Password) +func (r *adminRepo) UpdateAdmin(ctx context.Context, a *biz.Admin) (*biz.Admin, error) { + values := map[string]any{"name": a.Name, "email": a.Email, "access": a.Access, "avatar": a.Avatar, "update_time": time.Now()} + if a.Password != "" { + values["password"] = a.Password } - po, err := update.Save(ctx) - if err != nil { + if err := r.data.gormDB.WithContext(ctx).Model(&adminPO{}).Where("id = ?", a.ID).Updates(values).Error; err != nil { return nil, err } - return convertAdmin(po), nil + return r.FindByID(ctx, a.ID) } - func (r *adminRepo) DeleteAdmin(ctx context.Context, id int64) error { - return r.data.db.Admin.DeleteOneID(id).Exec(ctx) + return r.data.gormDB.WithContext(ctx).Delete(&adminPO{}, id).Error } diff --git a/internal/data/config_store.go b/internal/data/config_store.go new file mode 100644 index 0000000..b5f4184 --- /dev/null +++ b/internal/data/config_store.go @@ -0,0 +1,103 @@ +package data + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "gopkg.in/yaml.v3" +) + +func protoMap(message proto.Message) (map[string]any, error) { + raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(message) + if err != nil { + return nil, err + } + var value map[string]any + if err = json.Unmarshal(raw, &value); err != nil { + return nil, err + } + delete(value, "config_path") + return value, nil +} + +func setYAMLMapping(node *yaml.Node, key string, value any) error { + if node.Kind == yaml.DocumentNode { + node = node.Content[0] + } + if node.Kind != yaml.MappingNode { + return fmt.Errorf("configuration root is not a mapping") + } + raw, err := yaml.Marshal(value) + if err != nil { + return err + } + var replacement yaml.Node + if err = yaml.Unmarshal(raw, &replacement); err != nil { + return err + } + for i := 0; i < len(node.Content); i += 2 { + if node.Content[i].Value == key { + node.Content[i+1] = replacement.Content[0] + return nil + } + } + node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: key}, replacement.Content[0]) + return nil +} + +func (d *Data) persistConfig() error { + if d.admin == nil || d.admin.ConfigPath == "" { + return nil + } + raw, err := os.ReadFile(d.admin.ConfigPath) + if err != nil { + return err + } + var document yaml.Node + if err = yaml.Unmarshal(raw, &document); err != nil { + return err + } + dataValue, err := protoMap(d.config) + if err != nil { + return err + } + adminValue, err := protoMap(d.admin) + if err != nil { + return err + } + if err = setYAMLMapping(&document, "data", dataValue); err != nil { + return err + } + if err = setYAMLMapping(&document, "admin", adminValue); err != nil { + return err + } + output, err := yaml.Marshal(&document) + if err != nil { + return err + } + if err = os.MkdirAll(filepath.Dir(d.admin.ConfigPath), 0o755); err != nil { + return err + } + temporary, err := os.CreateTemp(filepath.Dir(d.admin.ConfigPath), ".kra-config-*.yaml") + if err != nil { + return err + } + tempName := temporary.Name() + defer os.Remove(tempName) + if _, err = temporary.Write(output); err != nil { + _ = temporary.Close() + return err + } + if err = temporary.Chmod(0o600); err != nil { + _ = temporary.Close() + return err + } + if err = temporary.Close(); err != nil { + return err + } + return os.Rename(tempName, d.admin.ConfigPath) +} diff --git a/internal/data/data.go b/internal/data/data.go index d7db429..cb1fe67 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -4,55 +4,47 @@ import ( "context" "fmt" "log" - "os" + "sync" "time" - _ "github.com/go-sql-driver/mysql" "github.com/google/wire" "github.com/redis/go-redis/v9" - "gorm.io/driver/mysql" "gorm.io/gorm" "kra/internal/conf" - "kra/internal/data/ent" - "kra/internal/data/ent/migrate" ) -// ProviderSet is data providers. var ProviderSet = wire.NewSet(NewData, NewAdminRepo, NewSystemRepo, NewAccessRepo, NewSettingsRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewTaskRepo, NewMediaRepo, NewAnnouncementRepo, NewEmailRepo, NewCache, NewFileStorage) -// Data is a struct that contains the database client. type Data struct { - db *ent.Client - gormDB *gorm.DB - redis *redis.Client + mu sync.RWMutex + gormDB *gorm.DB + redis *redis.Client + database *conf.Data_Database + config *conf.Data + admin *conf.AdminBackend } -// NewData creates a new Data instance. -func NewData(c *conf.Data) (*Data, func(), error) { +func NewData(c *conf.Data, admin *conf.AdminBackend) (*Data, func(), error) { if c == nil || c.Database == nil { return nil, nil, fmt.Errorf("database configuration is required") } - db, err := ent.Open(c.Database.Driver, c.Database.Source) + d := &Data{database: c.Database, config: c, admin: admin} + db, err := openDatabase(c.Database, false) if err != nil { - return nil, nil, fmt.Errorf("open ent database: %w", err) - } - gormDB, err := gorm.Open(mysql.Open(c.Database.Source), &gorm.Config{}) - if err != nil { - _ = db.Close() - return nil, nil, err - } - // The announcement module owns its table lifecycle independently of the - // first-run database initializer. - if err = gormDB.AutoMigrate(&announcementPO{}); err != nil { - _ = db.Close() - return nil, nil, fmt.Errorf("migrate announcement table: %w", err) - } - var redisClient *redis.Client - if c.Redis != nil && c.Redis.Addr != "" { - options := &redis.Options{Addr: c.Redis.Addr} - if c.Redis.Network != "" { - options.Network = c.Redis.Network + // The initialization endpoint must remain available when the configured + // target database has not been created yet. + log.Printf("configured database unavailable before initialization: %v", err) + db, err = openFallbackDatabase() + if err != nil { + return nil, nil, fmt.Errorf("open bootstrap database: %w", err) } + } + d.gormDB = db + if err = db.AutoMigrate(&announcementPO{}, &adminPO{}); err != nil { + return nil, nil, fmt.Errorf("migrate bootstrap tables: %w", err) + } + if c.Redis != nil && c.Redis.Addr != "" { + options := &redis.Options{Addr: c.Redis.Addr, Network: c.Redis.Network} if c.Redis.ReadTimeout != nil { options.ReadTimeout = c.Redis.ReadTimeout.AsDuration() } @@ -65,27 +57,42 @@ func NewData(c *conf.Data) (*Data, func(), error) { log.Printf("redis unavailable, using in-memory cache: %v", pingErr) _ = candidate.Close() } else { - redisClient = candidate + d.redis = candidate } cancel() } - if os.Getenv("DEPLOY_ENV") == "dev" { - // Enable debug mode for detailed logging. - db = db.Debug() - // Run the auto migration tool. - if err = db.Schema.Create(context.Background(), migrate.WithDropIndex(true)); err != nil { - return nil, nil, err - } - } cleanup := func() { - _ = db.Close() - if redisClient != nil { - _ = redisClient.Close() + d.mu.RLock() + db := d.gormDB + d.mu.RUnlock() + if sqlDB, closeErr := db.DB(); closeErr == nil { + _ = sqlDB.Close() + } + if d.redis != nil { + _ = d.redis.Close() } } - return &Data{ - db: db, - gormDB: gormDB, - redis: redisClient, - }, cleanup, nil + return d, cleanup, nil +} + +func (d *Data) switchDatabase(config *conf.Data_Database) error { + db, err := openDatabase(config, true) + if err != nil { + return err + } + d.mu.Lock() + old := d.gormDB + d.gormDB = db + d.database = config + d.config.Database = config + d.mu.Unlock() + if err := d.persistConfig(); err != nil { + return fmt.Errorf("persist database configuration: %w", err) + } + if old != nil { + if sqlDB, e := old.DB(); e == nil { + _ = sqlDB.Close() + } + } + return nil } diff --git a/internal/data/database.go b/internal/data/database.go new file mode 100644 index 0000000..61c639e --- /dev/null +++ b/internal/data/database.go @@ -0,0 +1,179 @@ +package data + +import ( + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" + + oracle "github.com/dzwvip/gorm-oracle" + "github.com/glebarez/sqlite" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlserver" + "gorm.io/gorm" + "kra/internal/conf" +) + +var databaseNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_$-]*$`) + +func normalizedDriver(driver string) string { + switch strings.ToLower(strings.TrimSpace(driver)) { + case "postgres", "postgresql", "pgsql": + return "pgsql" + case "sqlserver", "mssql": + return "mssql" + case "sqlite", "sqlite3": + return "sqlite" + case "oracle": + return "oracle" + default: + return "mysql" + } +} + +func databaseDSN(c *conf.Data_Database, name string) (string, error) { + if c.Source != "" && c.Host == "" && c.Path == "" { + return c.Source, nil + } + driver := normalizedDriver(c.Driver) + if name == "" { + name = c.Name + } + host := c.Host + if host == "" { + host = "127.0.0.1" + } + switch driver { + case "mysql": + port := c.Port + if port == "" { + port = "3306" + } + query := c.Config + if query == "" { + query = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4" + } + return fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", c.User, c.Password, net.JoinHostPort(host, port), name, query), nil + case "pgsql": + port := c.Port + if port == "" { + port = "5432" + } + extra := c.Config + if extra == "" { + extra = "sslmode=disable TimeZone=Local" + } + return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s %s", host, port, c.User, c.Password, name, extra), nil + case "mssql": + port := c.Port + if port == "" { + port = "1433" + } + q := url.Values{"database": {name}, "encrypt": {"disable"}} + return (&url.URL{Scheme: "sqlserver", User: url.UserPassword(c.User, c.Password), Host: net.JoinHostPort(host, port), RawQuery: q.Encode()}).String(), nil + case "oracle": + port := c.Port + if port == "" { + port = "1521" + } + return fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(c.User), url.PathEscape(c.Password), net.JoinHostPort(host, port), url.PathEscape(name), c.Config), nil + case "sqlite": + path := c.Path + if path == "" { + path = "." + } + if name == "" { + name = "kra" + } + if filepath.Ext(name) == "" { + name += ".db" + } + return filepath.Join(path, name), nil + } + return "", fmt.Errorf("unsupported database driver %q", c.Driver) +} + +func openWithDriver(driver, dsn string) (*gorm.DB, error) { + switch normalizedDriver(driver) { + case "mysql": + return gorm.Open(mysql.Open(dsn), &gorm.Config{}) + case "pgsql": + return gorm.Open(postgres.Open(dsn), &gorm.Config{}) + case "mssql": + return gorm.Open(sqlserver.Open(dsn), &gorm.Config{}) + case "oracle": + return gorm.Open(oracle.Open(dsn), &gorm.Config{}) + case "sqlite": + return gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + default: + return nil, fmt.Errorf("unsupported database driver %q", driver) + } +} + +func openDatabase(c *conf.Data_Database, create bool) (*gorm.DB, error) { + driver := normalizedDriver(c.Driver) + if driver == "sqlite" { + dsn, err := databaseDSN(c, "") + if err != nil { + return nil, err + } + if err = os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil { + return nil, err + } + return openWithDriver(driver, dsn) + } + if create && driver != "oracle" { + if !databaseNamePattern.MatchString(c.Name) { + return nil, fmt.Errorf("invalid database name %q", c.Name) + } + bootstrap := "" + switch driver { + case "pgsql": + bootstrap = "postgres" + case "mssql": + bootstrap = "master" + } + dsn, err := databaseDSN(c, bootstrap) + if err != nil { + return nil, err + } + adminDB, err := openWithDriver(driver, dsn) + if err != nil { + return nil, fmt.Errorf("connect database server: %w", err) + } + var statement string + switch driver { + case "mysql": + statement = "CREATE DATABASE IF NOT EXISTS `" + c.Name + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci" + case "pgsql": + var count int64 + if err = adminDB.Raw("SELECT count(*) FROM pg_database WHERE datname = ?", c.Name).Scan(&count).Error; err == nil && count == 0 { + statement = `CREATE DATABASE "` + c.Name + `"` + } + case "mssql": + statement = "IF DB_ID(N'" + c.Name + "') IS NULL CREATE DATABASE [" + c.Name + "]" + } + if statement != "" { + err = adminDB.Exec(statement).Error + } + if sqlDB, e := adminDB.DB(); e == nil { + _ = sqlDB.Close() + } + if err != nil { + return nil, fmt.Errorf("create database: %w", err) + } + } + dsn, err := databaseDSN(c, "") + if err != nil { + return nil, err + } + return openWithDriver(driver, dsn) +} + +func openFallbackDatabase() (*gorm.DB, error) { + return openWithDriver("sqlite", "file:kra-bootstrap?mode=memory&cache=shared") +} diff --git a/internal/data/file_storage.go b/internal/data/file_storage.go index 39cf7ee..9635c4e 100644 --- a/internal/data/file_storage.go +++ b/internal/data/file_storage.go @@ -21,6 +21,33 @@ type fileStorage struct { } func NewFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) { + storageType := "local" + if config != nil && config.Storage != nil && config.Storage.Type != "" { + storageType = strings.ToLower(config.Storage.Type) + } + if storageType == "qiniu" { + return newQiniuStorage(config.Storage.Qiniu) + } + if storageType != "local" { + var object *conf.AdminBackend_ObjectStore + switch storageType { + case "aliyun-oss": + object = config.Storage.AliyunOss + case "huawei-obs": + object = config.Storage.HuaweiObs + case "tencent-cos": + object = config.Storage.TencentCos + case "aws-s3": + object = config.Storage.AwsS3 + case "cloudflare-r2": + object = config.Storage.CloudflareR2 + case "minio": + object = config.Storage.Minio + default: + return nil, fmt.Errorf("unsupported storage type %q", storageType) + } + return newS3Storage(storageType, object) + } root, prefix := "uploads/file", "uploads/file" if config != nil && config.Local != nil { if config.Local.StorePath != "" { @@ -37,6 +64,43 @@ func NewFileStorage(config *conf.AdminBackend) (biz.FileStorage, error) { return &fileStorage{root: root, urlPrefix: "/" + strings.Trim(prefix, "/")}, nil } +func composeFiles(ctx context.Context, storage biz.FileStorage, names []string, destination string) (*biz.StoredFile, string, error) { + reader, writer := io.Pipe() + hash := md5.New() + errCh := make(chan error, 1) + go func() { + defer writer.Close() + for _, name := range names { + if ctx.Err() != nil { + errCh <- ctx.Err() + return + } + file, err := storage.Open(ctx, name) + if err != nil { + errCh <- err + return + } + _, err = io.Copy(io.MultiWriter(writer, hash), file) + _ = file.Close() + if err != nil { + errCh <- err + return + } + } + errCh <- nil + }() + stored, err := storage.Put(ctx, destination, reader) + composeErr := <-errCh + if err != nil { + return nil, "", err + } + if composeErr != nil { + _ = storage.Delete(ctx, destination) + return nil, "", composeErr + } + return stored, hex.EncodeToString(hash.Sum(nil)), nil +} + func (s *fileStorage) resolve(name string) (string, error) { clean := filepath.Clean(strings.TrimPrefix(name, "/")) if clean == "." || filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) { @@ -88,40 +152,7 @@ func (s *fileStorage) Delete(ctx context.Context, name string) error { } func (s *fileStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) { - reader, writer := io.Pipe() - hash := md5.New() - errCh := make(chan error, 1) - go func() { - defer writer.Close() - for _, name := range names { - if ctx.Err() != nil { - errCh <- ctx.Err() - return - } - file, err := s.Open(ctx, name) - if err != nil { - errCh <- err - return - } - _, err = io.Copy(io.MultiWriter(writer, hash), file) - _ = file.Close() - if err != nil { - errCh <- err - return - } - } - errCh <- nil - }() - stored, err := s.Put(ctx, destination, reader) - composeErr := <-errCh - if err != nil { - return nil, "", err - } - if composeErr != nil { - _ = s.Delete(ctx, destination) - return nil, "", composeErr - } - return stored, hex.EncodeToString(hash.Sum(nil)), nil + return composeFiles(ctx, s, names, destination) } func (s *fileStorage) DeletePrefix(ctx context.Context, prefix string) error { path, err := s.resolve(strings.TrimSuffix(prefix, "/") + "/placeholder") diff --git a/internal/data/media.go b/internal/data/media.go index 584a305..0120b5a 100644 --- a/internal/data/media.go +++ b/internal/data/media.go @@ -143,7 +143,7 @@ func (r *mediaRepo) DeleteMedia(ctx context.Context, id uint) error { } func (r *mediaRepo) MediaKeyReferences(ctx context.Context, key string) (int64, error) { var count int64 - err := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where("`key` = ?", key).Count(&count).Error + err := r.data.gormDB.WithContext(ctx).Model(&mediaPO{}).Where(map[string]any{"key": key}).Count(&count).Error return count, err } func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile) error { diff --git a/internal/data/qiniu_storage.go b/internal/data/qiniu_storage.go new file mode 100644 index 0000000..54ea980 --- /dev/null +++ b/internal/data/qiniu_storage.go @@ -0,0 +1,126 @@ +package data + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "path" + "strings" + "time" + + "github.com/qiniu/go-sdk/v7/auth/qbox" + qstorage "github.com/qiniu/go-sdk/v7/storage" + "kra/internal/biz" + "kra/internal/conf" +) + +type qiniuStorage struct { + config *conf.AdminBackend_Qiniu + upload *qstorage.FormUploader + manager *qstorage.BucketManager + mac *qbox.Mac + token string +} + +func newQiniuStorage(config *conf.AdminBackend_Qiniu) (biz.FileStorage, error) { + if config == nil || config.Bucket == "" || config.AccessKey == "" || config.SecretKey == "" { + return nil, fmt.Errorf("qiniu storage configuration is incomplete") + } + cfg := qstorage.Config{UseHTTPS: config.UseHttps, UseCdnDomains: config.UseCdnDomains} + switch config.Zone { + case "ZoneHuadong": + cfg.Zone = &qstorage.ZoneHuadong + case "ZoneHuabei": + cfg.Zone = &qstorage.ZoneHuabei + case "ZoneHuanan": + cfg.Zone = &qstorage.ZoneHuanan + case "ZoneBeimei": + cfg.Zone = &qstorage.ZoneBeimei + case "ZoneXinjiapo": + cfg.Zone = &qstorage.ZoneXinjiapo + } + mac := qbox.NewMac(config.AccessKey, config.SecretKey) + policy := qstorage.PutPolicy{Scope: config.Bucket} + token := policy.UploadToken(mac) + return &qiniuStorage{config: config, upload: qstorage.NewFormUploader(&cfg), manager: qstorage.NewBucketManager(mac, &cfg), mac: mac, token: token}, nil +} +func (s *qiniuStorage) file(key string, size int64) *biz.StoredFile { + return &biz.StoredFile{Name: path.Base(key), Path: key, URL: strings.TrimSuffix(s.config.BaseUrl, "/") + "/" + key, Size: size} +} +func (s *qiniuStorage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) { + temporary, err := os.CreateTemp("", "kra-qiniu-upload-*") + if err != nil { + return nil, err + } + temporaryName := temporary.Name() + defer os.Remove(temporaryName) + size, err := io.Copy(temporary, reader) + if err != nil { + _ = temporary.Close() + return nil, err + } + if _, err = temporary.Seek(0, io.SeekStart); err != nil { + _ = temporary.Close() + return nil, err + } + defer temporary.Close() + ret := qstorage.PutRet{} + if err = s.upload.Put(ctx, &ret, s.token, strings.TrimPrefix(name, "/"), temporary, size, &qstorage.PutExtra{}); err != nil { + return nil, err + } + return s.file(ret.Key, size), nil +} +func (s *qiniuStorage) Open(ctx context.Context, name string) (io.ReadCloser, error) { + downloadURL := qstorage.MakePrivateURL(s.mac, strings.TrimSuffix(s.config.BaseUrl, "/"), strings.TrimPrefix(name, "/"), time.Now().Add(time.Hour).Unix()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode >= 300 { + _ = resp.Body.Close() + return nil, fmt.Errorf("qiniu download failed: %s", resp.Status) + } + return resp.Body, nil +} +func (s *qiniuStorage) Delete(ctx context.Context, name string) error { + return s.manager.Delete(s.config.Bucket, strings.TrimPrefix(name, "/")) +} +func (s *qiniuStorage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) { + return composeFiles(ctx, s, names, destination) +} +func (s *qiniuStorage) DeletePrefix(ctx context.Context, prefix string) error { + for { + items, _, more, err := s.List(ctx, prefix, "", 1000) + if err != nil { + return err + } + for _, item := range items { + if err = s.Delete(ctx, item.Path); err != nil { + return err + } + } + if !more { + return nil + } + } +} +func (s *qiniuStorage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) { + if limit < 1 || limit > 1000 { + limit = 100 + } + entries, _, marker, more, err := s.manager.ListFiles(s.config.Bucket, prefix, "", cursor, limit) + if err != nil { + return nil, "", false, err + } + out := make([]*biz.StoredFile, 0, len(entries)) + for _, entry := range entries { + out = append(out, s.file(entry.Key, entry.Fsize)) + } + return out, marker, more, nil +} diff --git a/internal/data/s3_storage.go b/internal/data/s3_storage.go new file mode 100644 index 0000000..fe55e2c --- /dev/null +++ b/internal/data/s3_storage.go @@ -0,0 +1,138 @@ +package data + +import ( + "context" + "fmt" + "io" + "net/url" + "path" + "strings" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "kra/internal/biz" + "kra/internal/conf" +) + +type s3Storage struct { + client *minio.Client + bucket, baseURL, prefix string +} + +func newS3Storage(provider string, config *conf.AdminBackend_ObjectStore) (biz.FileStorage, error) { + if config == nil || config.Bucket == "" || config.AccessKey == "" || config.SecretKey == "" { + return nil, fmt.Errorf("%s storage configuration is incomplete", provider) + } + endpoint := config.Endpoint + if endpoint == "" { + switch provider { + case "aws-s3": + endpoint = "s3." + config.Region + ".amazonaws.com" + case "cloudflare-r2": + endpoint = config.AccountId + ".r2.cloudflarestorage.com" + case "aliyun-oss": + endpoint = "oss-" + config.Region + ".aliyuncs.com" + case "huawei-obs": + endpoint = "obs." + config.Region + ".myhuaweicloud.com" + case "tencent-cos": + endpoint = "cos." + config.Region + ".myqcloud.com" + } + } + secure := config.UseSsl + if parsed, err := url.Parse(endpoint); err == nil && parsed.Host != "" { + secure = parsed.Scheme == "https" + endpoint = parsed.Host + } + if endpoint == "" { + return nil, fmt.Errorf("%s endpoint is required", provider) + } + client, err := minio.New(strings.TrimSuffix(endpoint, "/"), &minio.Options{Creds: credentials.NewStaticV4(config.AccessKey, config.SecretKey, ""), Secure: secure, Region: config.Region, BucketLookup: func() minio.BucketLookupType { + if config.ForcePathStyle { + return minio.BucketLookupPath + } + return minio.BucketLookupAuto + }()}) + if err != nil { + return nil, err + } + return &s3Storage{client: client, bucket: config.Bucket, baseURL: strings.TrimSuffix(config.BaseUrl, "/"), prefix: strings.Trim(config.PathPrefix, "/")}, nil +} +func (s *s3Storage) key(name string) string { + if s.prefix == "" { + return strings.TrimPrefix(name, "/") + } + return path.Join(s.prefix, strings.TrimPrefix(name, "/")) +} +func (s *s3Storage) unkey(key string) string { + return strings.TrimPrefix(strings.TrimPrefix(key, s.prefix), "/") +} +func (s *s3Storage) file(key string, size int64) *biz.StoredFile { + name := s.unkey(key) + rawURL := s.baseURL + "/" + key + if s.baseURL == "" { + rawURL = key + } + return &biz.StoredFile{Name: path.Base(name), Path: name, URL: rawURL, Size: size} +} +func (s *s3Storage) Put(ctx context.Context, name string, reader io.Reader) (*biz.StoredFile, error) { + key := s.key(name) + info, err := s.client.PutObject(ctx, s.bucket, key, reader, -1, minio.PutObjectOptions{}) + if err != nil { + return nil, err + } + return s.file(key, info.Size), nil +} +func (s *s3Storage) Open(ctx context.Context, name string) (io.ReadCloser, error) { + obj, err := s.client.GetObject(ctx, s.bucket, s.key(name), minio.GetObjectOptions{}) + if err != nil { + return nil, err + } + if _, err = obj.Stat(); err != nil { + _ = obj.Close() + return nil, err + } + return obj, nil +} +func (s *s3Storage) Delete(ctx context.Context, name string) error { + return s.client.RemoveObject(ctx, s.bucket, s.key(name), minio.RemoveObjectOptions{}) +} +func (s *s3Storage) Compose(ctx context.Context, names []string, destination string) (*biz.StoredFile, string, error) { + return composeFiles(ctx, s, names, destination) +} +func (s *s3Storage) DeletePrefix(ctx context.Context, prefix string) error { + items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true}) + for item := range items { + if item.Err != nil { + return item.Err + } + if err := s.client.RemoveObject(ctx, s.bucket, item.Key, minio.RemoveObjectOptions{}); err != nil { + return err + } + } + return nil +} +func (s *s3Storage) List(ctx context.Context, prefix, cursor string, limit int) ([]*biz.StoredFile, string, bool, error) { + if limit < 1 || limit > 1000 { + limit = 100 + } + items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: s.key(prefix), Recursive: true, StartAfter: s.key(cursor)}) + out := make([]*biz.StoredFile, 0, limit+1) + for item := range items { + if item.Err != nil { + return nil, "", false, item.Err + } + out = append(out, s.file(item.Key, item.Size)) + if len(out) > limit { + break + } + } + more := len(out) > limit + if more { + out = out[:limit] + } + next := "" + if len(out) > 0 { + next = out[len(out)-1].Path + } + return out, next, more, nil +} diff --git a/internal/data/settings.go b/internal/data/settings.go index a54c783..de5596e 100644 --- a/internal/data/settings.go +++ b/internal/data/settings.go @@ -10,6 +10,7 @@ import ( "kra/internal/biz" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type dictionaryPO struct { @@ -334,7 +335,7 @@ func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) ( if id != 0 { err = db.First(&po, id).Error } else { - err = db.Where("`key` = ?", key).First(&po).Error + err = db.Where(map[string]any{"key": key}).First(&po).Error } if err != nil { return nil, err @@ -354,7 +355,7 @@ func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *bi db = db.Where("name LIKE ?", "%"+q.Name+"%") } if q.Key != "" { - db = db.Where("`key` LIKE ?", "%"+q.Key+"%") + db = db.Where(clause.Like{Column: clause.Column{Name: "key"}, Value: "%" + q.Key + "%"}) } } var total int64 diff --git a/internal/data/system.go b/internal/data/system.go index 0247743..6b7f6d8 100644 --- a/internal/data/system.go +++ b/internal/data/system.go @@ -7,6 +7,7 @@ import ( "time" "kra/internal/biz" + "kra/internal/conf" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" @@ -87,14 +88,22 @@ func (authorityMenuPO) TableName() string { return "sys_authority_menus" } type systemRepo struct{ data *Data } -func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} } +func NewSystemRepo(data *Data) biz.SystemRepo { return &systemRepo{data: data} } +func (r *systemRepo) PersistConfig(context.Context) error { return r.data.persistConfig() } func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) { return r.data.gormDB.WithContext(ctx).Migrator().HasTable(&userPO{}), nil } -func (r *systemRepo) Initialize(ctx context.Context) error { +func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error { + config := &conf.Data_Database{Driver: input.Driver, Host: input.Host, Port: input.Port, User: input.User, Password: input.Password, Name: input.Name, Path: input.Path, Config: input.Config} + if err := r.data.switchDatabase(config); err != nil { + return err + } db := r.data.gormDB.WithContext(ctx) + if err := db.AutoMigrate(&adminPO{}); err != nil { + return err + } if err := db.AutoMigrate(&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{}, &departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{}, &dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &securityConfigPO{}, &versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{}, &operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{}, &taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{}, &announcementPO{}); err != nil { return err } @@ -140,7 +149,7 @@ func (r *systemRepo) Initialize(ctx context.Context) error { return err } if count == 0 { - hash, err := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost) + hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost) if err != nil { return err } @@ -349,9 +358,13 @@ func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error { return tx.Delete(&userPO{}, id).Error }) } -func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string) error { +func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error { now := time.Now() - return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(map[string]any{"password": password, "password_updated_at": now, "must_change_password": false}).Error + updates := map[string]any{"password": password, "password_updated_at": now} + if clearMustChange { + updates["must_change_password"] = false + } + return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error } func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { var pos []authorityPO diff --git a/internal/server/audit_middleware.go b/internal/server/audit_middleware.go index 25969ec..199f9bc 100644 --- a/internal/server/audit_middleware.go +++ b/internal/server/audit_middleware.go @@ -62,7 +62,7 @@ func redactJSON(raw []byte) string { } func operationAudit(svc *service.AuditService) gin.HandlerFunc { return func(c *gin.Context) { - if c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" { + if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" { c.Next() return } diff --git a/internal/server/gin.go b/internal/server/gin.go index 5bf5482..4d7d3b5 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -34,7 +34,8 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System gin.SetMode(gin.ReleaseMode) engine := gin.New() engine.Use(ginRequestMeta(), emailErrorAlert(emails, logger), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit)) - if config != nil && config.Local != nil && config.Local.StorePath != "" { + localStorage := config == nil || config.Storage == nil || config.Storage.Type == "" || config.Storage.Type == "local" + if localStorage && config != nil && config.Local != nil && config.Local.StorePath != "" { pathPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/") if pathPrefix != "/" { engine.StaticFS(pathPrefix, filesOnly{FileSystem: http.Dir(config.Local.StorePath)}) @@ -51,11 +52,11 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System }) registerPublicRoutes(public, engine, config, svc, settings, access, audit) private := engine.Group(prefix) - private.Use(jwtAuth(config, settings), accessControl(access, audit)) + private.Use(jwtAuth(config, settings), mustChangePasswordGuard(), accessControl(access, audit)) registerPrivateRoutes(private, svc) registerAccessRoutes(private, engine, access) registerSettingsRoutes(private, public, settings) - registerSystemConfigRoutes(private, config, settings) + registerSystemConfigRoutes(private, config, svc, settings) registerVersionRoutes(private, versions) registerExportRoutes(private, public, svc, exports) registerAuditRoutes(private, public, audit) @@ -117,6 +118,22 @@ func currentClaims(c *gin.Context) *adminauth.Claims { return result } +func mustChangePasswordGuard() gin.HandlerFunc { + return func(c *gin.Context) { + claims := currentClaims(c) + if claims == nil || !claims.MustChangePwd { + c.Next() + return + } + path := strings.TrimSuffix(c.Request.URL.Path, "/") + if strings.HasSuffix(path, "/user/changePassword") || strings.HasSuffix(path, "/user/getUserInfo") || strings.HasSuffix(path, "/jwt/jsonInBlacklist") { + c.Next() + return + } + c.AbortWithStatusJSON(http.StatusConflict, response{Code: codePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "请先修改初始密码"}) + } +} + func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *conf.AdminBackend, svc *service.SystemService, settings *service.SettingsService, access *service.AccessService, audit *service.AuditService) { expiration := 3 * time.Minute keyLong, width, height := 6, 240, 80 @@ -221,8 +238,13 @@ func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *co writeResult(c, codeSuccess, gin.H{"needInit": !initialized}, message) }) initGroup.POST("/initdb", func(c *gin.Context) { - if err := svc.Initialize(c.Request.Context()); err != nil { - fail(c, "自动创建数据库失败") + var input service.DatabaseInit + if err := c.ShouldBindJSON(&input); err != nil { + fail(c, "数据库初始化参数无效") + return + } + if err := svc.Initialize(c.Request.Context(), &input); err != nil { + fail(c, "自动创建数据库失败: "+err.Error()) return } routes := engine.Routes() @@ -261,6 +283,10 @@ func incrementCached(ctx context.Context, svc *service.SystemService, key string } func securityRateLimit(svc *service.SystemService, settings *service.SettingsService) gin.HandlerFunc { return func(c *gin.Context) { + if isBootstrapPath(c.Request.URL.Path) { + c.Next() + return + } cfg, err := settings.CurrentSecurity(c.Request.Context()) if err != nil || cfg == nil || !cfg.LimitEnable { c.Next() @@ -281,6 +307,10 @@ func securityRateLimit(svc *service.SystemService, settings *service.SettingsSer } } +func isBootstrapPath(path string) bool { + return strings.HasSuffix(path, "/health") || strings.Contains(path, "/init/") +} + type captchaStore struct { service *service.SystemService expiration time.Duration diff --git a/internal/server/system_config_routes.go b/internal/server/system_config_routes.go index 64324d9..7f2dc94 100644 --- a/internal/server/system_config_routes.go +++ b/internal/server/system_config_routes.go @@ -3,15 +3,19 @@ package server import ( "runtime" "syscall" + "time" "kra/internal/biz" "kra/internal/conf" "kra/internal/service" "github.com/gin-gonic/gin" + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/mem" + "google.golang.org/protobuf/proto" ) -func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBackend, settings *service.SettingsService) { +func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBackend, systemService *service.SystemService, settings *service.SettingsService) { security := group.Group("/securityConfig") security.GET("/getSecurityConfig", func(c *gin.Context) { value, err := settings.Security(c.Request.Context()) @@ -53,6 +57,11 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken if config.Email != nil { email = gin.H{"to": config.Email.To, "from": config.Email.From, "host": config.Email.Host, "secret": "******", "nickname": config.Email.Nickname, "port": config.Email.Port, "is-ssl": config.Email.IsSsl, "is-loginauth": config.Email.IsLoginAuth} } + if config.Storage != nil { + storage := proto.Clone(config.Storage).(*conf.AdminBackend_Storage) + maskStorageSecrets(storage) + admin["storage"] = storage + } } writeResult(c, codeSuccess, gin.H{"config": gin.H{"admin": admin, "email": email}}, "获取成功") }) @@ -69,6 +78,7 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken StorePath string `json:"storePath"` PathPrefix string `json:"pathPrefix"` } `json:"local"` + Storage *conf.AdminBackend_Storage `json:"storage"` } `json:"admin"` Email *struct { To string `json:"to"` @@ -104,6 +114,10 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken config.Local.PathPrefix = req.Config.Admin.Local.PathPrefix } } + if req.Config.Admin.Storage != nil { + preserveStorageSecrets(req.Config.Admin.Storage, config.Storage) + config.Storage = req.Config.Admin.Storage + } if config.Email != nil && req.Config.Email != nil { config.Email.To = req.Config.Email.To config.Email.From = req.Config.Email.From @@ -117,12 +131,18 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken } } } - ok(c) + if err := systemService.PersistConfig(c.Request.Context()); err != nil { + fail(c, "配置保存失败: "+err.Error()) + return + } + writeResult(c, codeSuccess, gin.H{"restartRequired": true}, "配置已保存,重启服务后完整生效") + }) + system.POST("/reloadSystem", func(c *gin.Context) { + writeResult(c, codeSuccess, gin.H{"reload": false, "restartRequired": true}, "配置已在当前进程部分生效;路由与存储配置需重启服务") }) - system.POST("/reloadSystem", func(c *gin.Context) { writeResult(c, codeSuccess, gin.H{"reload": true}, "配置已热更新") }) system.POST("/getServerInfo", func(c *gin.Context) { - var mem runtime.MemStats - runtime.ReadMemStats(&mem) + cpuPercent, _ := cpu.Percent(200*time.Millisecond, false) + memory, _ := mem.VirtualMemory() disk := []gin.H{} var stat syscall.Statfs_t if syscall.Statfs(".", &stat) == nil { @@ -135,7 +155,42 @@ func registerSystemConfigRoutes(group *gin.RouterGroup, config *conf.AdminBacken } disk = append(disk, gin.H{"mountPoint": ".", "usedMb": used / 1024 / 1024, "usedGb": used / 1024 / 1024 / 1024, "totalMb": total / 1024 / 1024, "totalGb": total / 1024 / 1024 / 1024, "usedPercent": percent}) } - server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": []float64{}, "cores": runtime.NumCPU()}, "ram": gin.H{"usedMb": mem.Alloc / 1024 / 1024, "totalMb": mem.Sys / 1024 / 1024, "usedPercent": 0}, "disk": disk} + usedMB, totalMB, memoryPercent := uint64(0), uint64(0), float64(0) + if memory != nil { + usedMB, totalMB, memoryPercent = memory.Used/1024/1024, memory.Total/1024/1024, memory.UsedPercent + } + server := gin.H{"os": gin.H{"goos": runtime.GOOS, "numCpu": runtime.NumCPU(), "compiler": runtime.Compiler, "goVersion": runtime.Version(), "numGoroutine": runtime.NumGoroutine()}, "cpu": gin.H{"cpus": cpuPercent, "cores": runtime.NumCPU()}, "ram": gin.H{"usedMb": usedMB, "totalMb": totalMB, "usedPercent": memoryPercent}, "disk": disk} writeResult(c, codeSuccess, gin.H{"server": server}, "获取成功") }) } + +func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore { + if storage == nil { + return nil + } + return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio} +} +func maskStorageSecrets(storage *conf.AdminBackend_Storage) { + if storage.Qiniu != nil { + storage.Qiniu.SecretKey = "******" + } + for _, item := range objectStores(storage) { + if item != nil { + item.SecretKey = "******" + } + } +} +func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) { + if next == nil || current == nil { + return + } + if next.Qiniu != nil && current.Qiniu != nil && (next.Qiniu.SecretKey == "" || next.Qiniu.SecretKey == "******") { + next.Qiniu.SecretKey = current.Qiniu.SecretKey + } + nextItems, currentItems := objectStores(next), objectStores(current) + for i := range nextItems { + if nextItems[i] != nil && currentItems[i] != nil && (nextItems[i].SecretKey == "" || nextItems[i].SecretKey == "******") { + nextItems[i].SecretKey = currentItems[i].SecretKey + } + } +} diff --git a/internal/service/system.go b/internal/service/system.go index 0c707b4..7cddf4f 100644 --- a/internal/service/system.go +++ b/internal/service/system.go @@ -29,7 +29,30 @@ func NewSystemService(uc *biz.SystemUsecase, config *conf.AdminBackend, settings func (s *SystemService) IsInitialized(ctx context.Context) (bool, error) { return s.uc.IsInitialized(ctx) } -func (s *SystemService) Initialize(ctx context.Context) error { return s.uc.Initialize(ctx) } +func (s *SystemService) PersistConfig(ctx context.Context) error { return s.uc.PersistConfig(ctx) } + +type DatabaseInit struct { + DBType string `json:"dbType"` + Host string `json:"host"` + Port string `json:"port"` + UserName string `json:"userName"` + Password string `json:"password"` + DBName string `json:"dbName"` + DBPath string `json:"dbPath"` + Template string `json:"template"` + AdminPassword string `json:"adminPassword"` +} + +func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit) error { + config := "" + switch input.DBType { + case "mysql": + config = "timeout=5s&parseTime=True&loc=Local&charset=utf8mb4" + case "pgsql": + config = "sslmode=disable TimeZone=Local" + } + return s.uc.Initialize(ctx, &biz.DatabaseConfig{Driver: input.DBType, Host: input.Host, Port: input.Port, User: input.UserName, Password: input.Password, Name: input.DBName, Path: input.DBPath, Config: config, AdminPassword: input.AdminPassword}) +} func (s *SystemService) CacheGet(ctx context.Context, key string) (string, bool, error) { return s.uc.CacheGet(ctx, key) diff --git a/web/package.json b/web/package.json index c61d4a1..806ee45 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,6 @@ "axios": "1.8.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "echarts": "5.5.1", "element-plus": "^2.13.6", "highlight.js": "^11.10.0", "mitt": "^3.0.1", @@ -43,7 +42,6 @@ "vite-auto-import-svg": "^2.9.8", "vue": "^3.5.31", "vue-cropper": "^1.1.4", - "vue-echarts": "^7.0.3", "vue-qr": "^4.0.9", "vue-router": "^4.4.3", "vuedraggable": "^4.1.0" diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 93f2b20..59a8d08 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -53,9 +53,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - echarts: - specifier: 5.5.1 - version: 5.5.1 element-plus: specifier: ^2.13.6 version: 2.14.4(vue@3.5.41) @@ -101,9 +98,6 @@ importers: vue-cropper: specifier: ^1.1.4 version: 1.1.4 - vue-echarts: - specifier: ^7.0.3 - version: 7.0.3(@vue/runtime-core@3.5.41)(echarts@5.5.1)(vue@3.5.41) vue-qr: specifier: ^4.0.9 version: 4.0.9 @@ -1604,9 +1598,6 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - echarts@5.5.1: - resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==} - electron-to-chromium@1.5.405: resolution: {integrity: sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==} @@ -2473,9 +2464,6 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tslib@2.3.0: - resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2621,17 +2609,6 @@ packages: vue-cropper@1.1.4: resolution: {integrity: sha512-5m98vBsCEI9rbS4JxELxXidtAui3qNyTHLHg67Qbn7g8cg+E6LcnC+hh3SM/p94x6mFh6KRxT1ttnta+wCYqWA==} - vue-demi@0.13.11: - resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -2643,16 +2620,6 @@ packages: '@vue/composition-api': optional: true - vue-echarts@7.0.3: - resolution: {integrity: sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==} - peerDependencies: - '@vue/runtime-core': ^3.0.0 - echarts: ^5.5.1 - vue: ^2.7.0 || ^3.1.1 - peerDependenciesMeta: - '@vue/runtime-core': - optional: true - vue-eslint-parser@9.4.3: resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -2710,9 +2677,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zrender@5.6.0: - resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==} - snapshots: '@babel/code-frame@7.29.7': @@ -4295,11 +4259,6 @@ snapshots: duplexer@0.1.2: {} - echarts@5.5.1: - dependencies: - tslib: 2.3.0 - zrender: 5.6.0 - electron-to-chromium@1.5.405: {} element-plus@2.14.4(vue@3.5.41): @@ -5125,8 +5084,6 @@ snapshots: totalist@3.0.1: {} - tslib@2.3.0: {} - tslib@2.8.1: {} type-check@0.4.0: @@ -5258,24 +5215,10 @@ snapshots: vue-cropper@1.1.4: {} - vue-demi@0.13.11(vue@3.5.41): - dependencies: - vue: 3.5.41 - vue-demi@0.14.10(vue@3.5.41): dependencies: vue: 3.5.41 - vue-echarts@7.0.3(@vue/runtime-core@3.5.41)(echarts@5.5.1)(vue@3.5.41): - dependencies: - echarts: 5.5.1 - vue: 3.5.41 - vue-demi: 0.13.11(vue@3.5.41) - optionalDependencies: - '@vue/runtime-core': 3.5.41 - transitivePeerDependencies: - - '@vue/composition-api' - vue-eslint-parser@9.4.3(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0): dependencies: debug: 4.4.3(supports-color@7.2.0) @@ -5334,7 +5277,3 @@ snapshots: yallist@3.1.1: {} yocto-queue@0.1.0: {} - - zrender@5.6.0: - dependencies: - tslib: 2.3.0 diff --git a/web/src/api/api.js b/web/src/api/api.js index 8ccbbaf..664648f 100644 --- a/web/src/api/api.js +++ b/web/src/api/api.js @@ -76,14 +76,6 @@ export const updateApi = (data) => { // @Param data body api.CreateApiParams true "更新api" // @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}" // @Router /api/setAuthApi [post] -export const setAuthApi = (data) => { - return service({ - url: '/api/setAuthApi', - method: 'post', - data - }) -} - // @Tags Api // @Summary 获取所有的Api 不分页 // @Security ApiKeyAuth diff --git a/web/src/api/system/sysError.js b/web/src/api/system/sysError.js index 6452b45..0d31820 100644 --- a/web/src/api/system/sysError.js +++ b/web/src/api/system/sysError.js @@ -102,9 +102,3 @@ export const getSysErrorList = (params) => { // @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表" // @Success 200 {object} response.Response{data=object,msg=string} "获取成功" // @Router /sysError/getSysErrorPublic [get] -export const getSysErrorPublic = () => { - return service({ - url: '/sysError/getSysErrorPublic', - method: 'get', - }) -} diff --git a/web/src/components/charts/index.vue b/web/src/components/charts/index.vue deleted file mode 100644 index 9eb71cb..0000000 --- a/web/src/components/charts/index.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/web/src/core/componentLibrary/index.js b/web/src/core/componentLibrary/index.js index 459b780..20a2edf 100644 --- a/web/src/core/componentLibrary/index.js +++ b/web/src/core/componentLibrary/index.js @@ -1,5 +1,5 @@ // 基础组件库:以无样式的 reka-ui 原语为底座,仅用项目既有的 UnoCSS 主题 token 上色, -// 跟随 themeStore 换肤 / 暗色模式。规范见 aiDoc/frontend-backend/component-library.md。 +// 跟随 themeStore 换肤与暗色模式。 export * from './button' export * from './input' export * from './checkbox' diff --git a/web/src/core/global.js b/web/src/core/global.js index 8b469be..bb792b5 100644 --- a/web/src/core/global.js +++ b/web/src/core/global.js @@ -6,7 +6,6 @@ import * as ElIconModules from '@element-plus/icons-vue' import svgIcon from '@/components/svgIcon/svgIcon.vue' // 基础组件库(reka-ui 底座,src/core/componentLibrary):全局以 g- 前缀(kebab-case)注册, // 全站可直接 / 使用,无需逐文件 import。 -// 规范见 aiDoc/frontend-backend/component-library.md import * as ComponentLibrary from '@/core/componentLibrary' // 导入转换图标名称的函数 diff --git a/web/src/view/dashboard/components/charts-content-numbers.vue b/web/src/view/dashboard/components/charts-content-numbers.vue deleted file mode 100644 index 2fbbcd8..0000000 --- a/web/src/view/dashboard/components/charts-content-numbers.vue +++ /dev/null @@ -1,187 +0,0 @@ - - - - - diff --git a/web/src/view/dashboard/components/charts-people-numbers.vue b/web/src/view/dashboard/components/charts-people-numbers.vue deleted file mode 100644 index 9b3c2d1..0000000 --- a/web/src/view/dashboard/components/charts-people-numbers.vue +++ /dev/null @@ -1,132 +0,0 @@ - - - - - diff --git a/web/src/view/dashboard/components/charts.vue b/web/src/view/dashboard/components/charts.vue deleted file mode 100644 index ddc52f6..0000000 --- a/web/src/view/dashboard/components/charts.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - diff --git a/web/src/view/dashboard/components/index.js b/web/src/view/dashboard/components/index.js index 160a448..2e5e5ca 100644 --- a/web/src/view/dashboard/components/index.js +++ b/web/src/view/dashboard/components/index.js @@ -1,11 +1,9 @@ import KraCard from './card.vue' -import KraChart from './charts.vue' import KraNotice from './notice.vue' import KraQuickLink from './quickLinks.vue' export { KraCard, - KraChart, KraNotice, KraQuickLink, } diff --git a/web/src/view/dashboard/components/notice.vue b/web/src/view/dashboard/components/notice.vue index 941a7da..e2c0a8c 100644 --- a/web/src/view/dashboard/components/notice.vue +++ b/web/src/view/dashboard/components/notice.vue @@ -30,37 +30,20 @@ diff --git a/web/src/view/dashboard/index.vue b/web/src/view/dashboard/index.vue index 7c221c6..f59fd1c 100644 --- a/web/src/view/dashboard/index.vue +++ b/web/src/view/dashboard/index.vue @@ -1,81 +1,38 @@ -