fix
This commit is contained in:
parent
e0b0e1d8c8
commit
f63acae353
|
|
@ -51,13 +51,6 @@ func newApp(logger *slog.Logger, hs *kratoshttp.Server, scheduler *worker.TaskSc
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
logger, closeLogger := logging.NewZapLogger("logs", "application.log",
|
|
||||||
slog.String("service.id", id),
|
|
||||||
slog.String("service.name", Name),
|
|
||||||
slog.String("service.version", Version),
|
|
||||||
)
|
|
||||||
defer closeLogger()
|
|
||||||
log.SetDefault(logger)
|
|
||||||
c := config.New(
|
c := config.New(
|
||||||
config.WithSource(
|
config.WithSource(
|
||||||
file.NewSource(flagconf),
|
file.NewSource(flagconf),
|
||||||
|
|
@ -73,6 +66,22 @@ func main() {
|
||||||
if err := c.Scan(&bc); err != nil {
|
if err := c.Scan(&bc); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
logOptions := logging.Options{Level: "info", Format: "json", EncodeLevel: "LowercaseLevelEncoder", LogInConsole: true, ShowLine: true, RetentionDay: 7}
|
||||||
|
logRoot := "logs"
|
||||||
|
if bc.Admin != nil && bc.Admin.Zap != nil {
|
||||||
|
zapConfig := bc.Admin.Zap
|
||||||
|
logOptions = logging.Options{Level: zapConfig.Level, Format: zapConfig.Format, EncodeLevel: zapConfig.EncodeLevel, Prefix: zapConfig.Prefix, StacktraceKey: zapConfig.StacktraceKey, LogInConsole: zapConfig.LogInConsole, ShowLine: zapConfig.ShowLine, RetentionDay: int(zapConfig.RetentionDay), FileOnlyModules: zapConfig.FileOnlyModules}
|
||||||
|
if zapConfig.Director != "" {
|
||||||
|
logRoot = zapConfig.Director
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loggerAttrs := []any{slog.String("service.id", id), slog.String("service.name", Name), slog.String("service.version", Version)}
|
||||||
|
if bc.Admin != nil && bc.Admin.App != nil {
|
||||||
|
loggerAttrs = append(loggerAttrs, slog.String("node", bc.Admin.App.Node), slog.String("app_id", bc.Admin.App.AppId), slog.String("env", bc.Admin.App.Env))
|
||||||
|
}
|
||||||
|
logger, closeLogger := logging.NewZapLogger(logRoot, "application.log", logOptions, loggerAttrs...)
|
||||||
|
defer closeLogger()
|
||||||
|
log.SetDefault(logger)
|
||||||
if bc.Admin != nil {
|
if bc.Admin != nil {
|
||||||
bc.Admin.ConfigPath = flagconf
|
bc.Admin.ConfigPath = flagconf
|
||||||
if info, err := os.Stat(flagconf); err == nil && info.IsDir() {
|
if info, err := os.Stat(flagconf); err == nil && info.IsDir() {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
|
||||||
systemService := service.NewSystemService(systemUsecase, runtime, settingsService)
|
systemService := service.NewSystemService(systemUsecase, runtime, settingsService)
|
||||||
accessRepo := data.NewAccessRepo(dataData)
|
accessRepo := data.NewAccessRepo(dataData)
|
||||||
accessUsecase := biz.NewAccessUsecase(accessRepo)
|
accessUsecase := biz.NewAccessUsecase(accessRepo)
|
||||||
accessService := service.NewAccessService(accessUsecase)
|
accessService := service.NewAccessService(accessUsecase, runtime)
|
||||||
authority := handler.NewAuthority(accessService)
|
authority := handler.NewAuthority(accessService)
|
||||||
menuRepo := data.NewMenuRepo(dataData)
|
menuRepo := data.NewMenuRepo(dataData)
|
||||||
menuUsecase := biz.NewMenuUsecase(menuRepo)
|
menuUsecase := biz.NewMenuUsecase(menuRepo)
|
||||||
|
|
@ -94,7 +94,7 @@ func wireApp(confServer *conf.Server, confData *conf.Data, adminBackend *conf.Ad
|
||||||
user := handler.NewUser(systemService)
|
user := handler.NewUser(systemService)
|
||||||
navigation := handler.NewNavigation(systemService)
|
navigation := handler.NewNavigation(systemService)
|
||||||
session := handler.NewSession(settingsService)
|
session := handler.NewSession(settingsService)
|
||||||
httpServer := server.NewGinServer(confServer, runtime, systemService, accessService, authority, menu, api, permission, organization, announcement, email, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, settingsService, auditService, emailService, logger)
|
httpServer := server.NewGinServer(confServer, runtime, systemService, accessService, authority, menu, api, permission, organization, announcement, email, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, settingsService, auditService, logger)
|
||||||
app := newApp(logger, httpServer, taskScheduler)
|
app := newApp(logger, httpServer, taskScheduler)
|
||||||
return app, func() {
|
return app, func() {
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,26 @@ data:
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: "3306"
|
port: "3306"
|
||||||
user: root
|
user: root
|
||||||
password: root
|
password: "12345678"
|
||||||
name: test
|
name: test
|
||||||
config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
|
config: timeout=5s&parseTime=True&loc=Local&charset=utf8mb4
|
||||||
|
log_mode: info
|
||||||
|
max_idle_conns: 10
|
||||||
|
max_open_conns: 100
|
||||||
|
conn_max_lifetime: 3600
|
||||||
redis:
|
redis:
|
||||||
|
name: default
|
||||||
addr: 127.0.0.1:6379
|
addr: 127.0.0.1:6379
|
||||||
|
password: ""
|
||||||
|
db: 0
|
||||||
|
use_cluster: false
|
||||||
|
cluster_addrs: []
|
||||||
read_timeout: 0.2s
|
read_timeout: 0.2s
|
||||||
write_timeout: 0.2s
|
write_timeout: 0.2s
|
||||||
database_list: []
|
database_list: []
|
||||||
|
redis_list: []
|
||||||
|
mongo:
|
||||||
|
hosts: []
|
||||||
admin:
|
admin:
|
||||||
router_prefix: ""
|
router_prefix: ""
|
||||||
jwt:
|
jwt:
|
||||||
|
|
@ -40,6 +52,29 @@ admin:
|
||||||
use_multipoint: false
|
use_multipoint: false
|
||||||
use_strict_auth: false
|
use_strict_auth: false
|
||||||
disable_auto_migrate: false
|
disable_auto_migrate: false
|
||||||
|
use_mongo: false
|
||||||
|
zap:
|
||||||
|
level: info
|
||||||
|
prefix: "[kra] "
|
||||||
|
format: json
|
||||||
|
director: logs
|
||||||
|
encode_level: LowercaseLevelEncoder
|
||||||
|
stacktrace_key: stacktrace
|
||||||
|
show_line: true
|
||||||
|
log_in_console: true
|
||||||
|
retention_day: 7
|
||||||
|
access_req_body: true
|
||||||
|
access_resp_data: true
|
||||||
|
access_req_headers: false
|
||||||
|
access_log_max_bytes: 32768
|
||||||
|
file_only_modules: []
|
||||||
|
cors:
|
||||||
|
mode: whitelist
|
||||||
|
whitelist: []
|
||||||
|
app:
|
||||||
|
node: ""
|
||||||
|
app_id: kra
|
||||||
|
env: development
|
||||||
disk_list:
|
disk_list:
|
||||||
- mount_point: /
|
- mount_point: /
|
||||||
storage:
|
storage:
|
||||||
|
|
|
||||||
7
go.mod
7
go.mod
|
|
@ -29,6 +29,7 @@ require (
|
||||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.60
|
github.com/tencentyun/cos-go-sdk-v5 v0.7.60
|
||||||
github.com/xuri/excelize/v2 v2.9.0
|
github.com/xuri/excelize/v2 v2.9.0
|
||||||
go.einride.tech/aip v0.86.3
|
go.einride.tech/aip v0.86.3
|
||||||
|
go.mongodb.org/mongo-driver v1.17.2
|
||||||
go.uber.org/automaxprocs v1.6.0
|
go.uber.org/automaxprocs v1.6.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go.uber.org/zap/exp v0.3.0
|
go.uber.org/zap/exp v0.3.0
|
||||||
|
|
@ -92,6 +93,7 @@ require (
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/go-querystring v1.0.0 // indirect
|
github.com/google/go-querystring v1.0.0 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
|
@ -114,6 +116,7 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/mozillazg/go-httpheader v0.2.1 // indirect
|
github.com/mozillazg/go-httpheader v0.2.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||||
|
|
@ -129,8 +132,12 @@ require (
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
|
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
|
||||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
|
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel v1.44.0 // indirect
|
go.opentelemetry.io/otel v1.44.0 // indirect
|
||||||
|
|
|
||||||
15
go.sum
15
go.sum
|
|
@ -166,6 +166,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
|
@ -262,6 +264,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd
|
||||||
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
|
github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg=
|
||||||
github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4=
|
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/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
|
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
|
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
|
||||||
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
|
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
|
@ -337,17 +341,27 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||||
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
|
||||||
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
|
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
|
||||||
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
|
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
|
||||||
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
|
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/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.einride.tech/aip v0.86.3 h1:jg80Ec4XBPYg1i7avzrl3MJol/dUwmMMLHtcmEMyxgM=
|
go.einride.tech/aip v0.86.3 h1:jg80Ec4XBPYg1i7avzrl3MJol/dUwmMMLHtcmEMyxgM=
|
||||||
go.einride.tech/aip v0.86.3/go.mod h1:dZuN/0sXeoscfWqsW8QLcLrGZdvsCC1B2R2CZ4kHmao=
|
go.einride.tech/aip v0.86.3/go.mod h1:dZuN/0sXeoscfWqsW8QLcLrGZdvsCC1B2R2CZ4kHmao=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||||
|
|
@ -470,6 +484,7 @@ 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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,6 @@ func (uc *SettingsUsecase) PrepareAPIToken(ctx context.Context, userID, authorit
|
||||||
if days == -1 {
|
if days == -1 {
|
||||||
duration = 100 * 365 * 24 * time.Hour
|
duration = 100 * 365 * 24 * time.Hour
|
||||||
}
|
}
|
||||||
if duration <= 0 {
|
|
||||||
return nil, 0, errors.New("有效天数必须大于0或为-1")
|
|
||||||
}
|
|
||||||
return user, duration, nil
|
return user, duration, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,15 @@ import "context"
|
||||||
|
|
||||||
type DataScope struct {
|
type DataScope struct {
|
||||||
All bool
|
All bool
|
||||||
|
Scope int
|
||||||
|
UserID uint
|
||||||
|
AuthorityID uint
|
||||||
|
PrimaryDeptID uint
|
||||||
OwnerUserID uint
|
OwnerUserID uint
|
||||||
DepartmentIDs []uint
|
DepartmentIDs []uint
|
||||||
|
RequestID string
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
type dataScopeKey struct{}
|
type dataScopeKey struct{}
|
||||||
|
|
|
||||||
|
|
@ -16,23 +16,8 @@ type EmailUsecase struct{ repo EmailRepo }
|
||||||
|
|
||||||
func NewEmailUsecase(repo EmailRepo) *EmailUsecase { return &EmailUsecase{repo: repo} }
|
func NewEmailUsecase(repo EmailRepo) *EmailUsecase { return &EmailUsecase{repo: repo} }
|
||||||
|
|
||||||
func splitRecipients(value string) []string {
|
|
||||||
parts := strings.Split(value, ",")
|
|
||||||
result := make([]string, 0, len(parts))
|
|
||||||
for _, part := range parts {
|
|
||||||
if recipient := strings.TrimSpace(part); recipient != "" {
|
|
||||||
result = append(result, recipient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *EmailUsecase) Send(ctx context.Context, to, subject, body string) error {
|
func (uc *EmailUsecase) Send(ctx context.Context, to, subject, body string) error {
|
||||||
recipients := splitRecipients(to)
|
return uc.repo.Send(ctx, strings.Split(to, ","), subject, body)
|
||||||
if len(recipients) == 0 || subject == "" {
|
|
||||||
return errors.New("收件人和邮件标题不能为空")
|
|
||||||
}
|
|
||||||
return uc.repo.Send(ctx, recipients, subject, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *EmailUsecase) Test(ctx context.Context) error {
|
func (uc *EmailUsecase) Test(ctx context.Context) error {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package biz
|
package biz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -50,9 +52,13 @@ func (uc *MediaUsecase) Upload(ctx context.Context, userID uint, name, mime stri
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ext := strings.ToLower(filepath.Ext(name))
|
ext := strings.ToLower(filepath.Ext(name))
|
||||||
|
buffered := bufio.NewReader(reader)
|
||||||
|
if header, _ := buffered.Peek(512); len(header) > 0 {
|
||||||
|
mime = http.DetectContentType(header)
|
||||||
|
}
|
||||||
key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
|
key := time.Now().Format("20060102") + "/" + uuid.NewString() + ext
|
||||||
hash := md5.New()
|
hash := md5.New()
|
||||||
stored, err := uc.files.Put(ctx, key, io.TeeReader(reader, hash))
|
stored, err := uc.files.Put(ctx, key, io.TeeReader(buffered, hash))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,6 @@ type SecurityRepo interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *SettingsUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error {
|
func (uc *SettingsUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error {
|
||||||
if value.KeyLong < 1 || value.ImgWidth < 1 || value.ImgHeight < 1 || value.PwdMinLength < 1 || value.LimitWindow < 1 || value.LimitCount < 1 || value.LockThreshold < 1 || value.LockDuration < 1 || value.PwdExpireDays < 1 {
|
|
||||||
return errors.New("安全配置数值必须大于0")
|
|
||||||
}
|
|
||||||
return uc.SaveSecurityConfig(ctx, value)
|
return uc.SaveSecurityConfig(ctx, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,10 +1,10 @@
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package kratos.api;
|
package kratos.api;
|
||||||
|
|
||||||
option go_package = "kra/internal/conf;conf";
|
|
||||||
|
|
||||||
import "google/protobuf/duration.proto";
|
import "google/protobuf/duration.proto";
|
||||||
|
|
||||||
|
option go_package = "kra/internal/conf;conf";
|
||||||
|
|
||||||
message Bootstrap {
|
message Bootstrap {
|
||||||
Server server = 1;
|
Server server = 1;
|
||||||
Data data = 2;
|
Data data = 2;
|
||||||
|
|
@ -33,16 +33,48 @@ message Data {
|
||||||
string path = 9;
|
string path = 9;
|
||||||
string alias_name = 10;
|
string alias_name = 10;
|
||||||
bool disable = 11;
|
bool disable = 11;
|
||||||
|
string prefix = 12;
|
||||||
|
string engine = 13;
|
||||||
|
string log_mode = 14;
|
||||||
|
int32 max_idle_conns = 15;
|
||||||
|
int32 max_open_conns = 16;
|
||||||
|
int32 conn_max_lifetime = 17;
|
||||||
|
bool singular = 18;
|
||||||
}
|
}
|
||||||
message Redis {
|
message Redis {
|
||||||
string network = 1;
|
string network = 1;
|
||||||
string addr = 2;
|
string addr = 2;
|
||||||
google.protobuf.Duration read_timeout = 3;
|
google.protobuf.Duration read_timeout = 3;
|
||||||
google.protobuf.Duration write_timeout = 4;
|
google.protobuf.Duration write_timeout = 4;
|
||||||
|
string name = 5;
|
||||||
|
string password = 6;
|
||||||
|
int32 db = 7;
|
||||||
|
bool use_cluster = 8;
|
||||||
|
repeated string cluster_addrs = 9;
|
||||||
|
}
|
||||||
|
message MongoHost {
|
||||||
|
string host = 1;
|
||||||
|
string port = 2;
|
||||||
|
}
|
||||||
|
message Mongo {
|
||||||
|
string coll = 1;
|
||||||
|
string options = 2;
|
||||||
|
string database = 3;
|
||||||
|
string username = 4;
|
||||||
|
string password = 5;
|
||||||
|
string auth_source = 6;
|
||||||
|
uint64 min_pool_size = 7;
|
||||||
|
uint64 max_pool_size = 8;
|
||||||
|
int64 socket_timeout_ms = 9;
|
||||||
|
int64 connect_timeout_ms = 10;
|
||||||
|
bool is_zap = 11;
|
||||||
|
repeated MongoHost hosts = 12;
|
||||||
}
|
}
|
||||||
Database database = 1;
|
Database database = 1;
|
||||||
Redis redis = 2;
|
Redis redis = 2;
|
||||||
repeated Database database_list = 3;
|
repeated Database database_list = 3;
|
||||||
|
repeated Redis redis_list = 4;
|
||||||
|
Mongo mongo = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminBackend contains settings for the administration HTTP transport.
|
// AdminBackend contains settings for the administration HTTP transport.
|
||||||
|
|
@ -58,6 +90,9 @@ message AdminBackend {
|
||||||
Media media = 8;
|
Media media = 8;
|
||||||
repeated Disk disk_list = 9;
|
repeated Disk disk_list = 9;
|
||||||
System system = 10;
|
System system = 10;
|
||||||
|
Zap zap = 11;
|
||||||
|
CORS cors = 12;
|
||||||
|
App app = 13;
|
||||||
|
|
||||||
message JWT {
|
message JWT {
|
||||||
string signing_key = 1;
|
string signing_key = 1;
|
||||||
|
|
@ -102,6 +137,41 @@ message AdminBackend {
|
||||||
bool use_multipoint = 2;
|
bool use_multipoint = 2;
|
||||||
bool use_strict_auth = 3;
|
bool use_strict_auth = 3;
|
||||||
bool disable_auto_migrate = 4;
|
bool disable_auto_migrate = 4;
|
||||||
|
bool use_mongo = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Zap {
|
||||||
|
string level = 1;
|
||||||
|
string prefix = 2;
|
||||||
|
string format = 3;
|
||||||
|
string director = 4;
|
||||||
|
string encode_level = 5;
|
||||||
|
string stacktrace_key = 6;
|
||||||
|
bool show_line = 7;
|
||||||
|
bool log_in_console = 8;
|
||||||
|
int32 retention_day = 9;
|
||||||
|
bool access_req_body = 10;
|
||||||
|
bool access_resp_data = 11;
|
||||||
|
bool access_req_headers = 12;
|
||||||
|
int32 access_log_max_bytes = 13;
|
||||||
|
repeated string file_only_modules = 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CORS {
|
||||||
|
string mode = 1;
|
||||||
|
repeated CORSRule whitelist = 2;
|
||||||
|
}
|
||||||
|
message CORSRule {
|
||||||
|
string allow_origin = 1;
|
||||||
|
string allow_methods = 2;
|
||||||
|
string allow_headers = 3;
|
||||||
|
string expose_headers = 4;
|
||||||
|
bool allow_credentials = 5;
|
||||||
|
}
|
||||||
|
message App {
|
||||||
|
string node = 1;
|
||||||
|
string app_id = 2;
|
||||||
|
string env = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Storage {
|
message Storage {
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ type apiPO struct {
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
Path string `gorm:"uniqueIndex:idx_api_path_method"`
|
Path string
|
||||||
Description string
|
Description string
|
||||||
APIGroup string `gorm:"column:api_group"`
|
APIGroup string `gorm:"column:api_group"`
|
||||||
Method string `gorm:"uniqueIndex:idx_api_path_method"`
|
Method string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (apiPO) TableName() string { return "sys_apis" }
|
func (apiPO) TableName() string { return "sys_apis" }
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ type apiTokenPO struct {
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
UserID uint
|
UserID uint
|
||||||
AuthorityID uint
|
AuthorityID uint
|
||||||
Token string `gorm:"type:text;uniqueIndex"`
|
Token string `gorm:"type:text"`
|
||||||
Status bool
|
Status bool
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
Remark string
|
Remark string
|
||||||
|
|
|
||||||
|
|
@ -318,53 +318,52 @@ func (r *accessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uin
|
||||||
return ids, err
|
return ids, err
|
||||||
}
|
}
|
||||||
func (r *accessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
|
func (r *accessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
|
||||||
if authorityID == 888 {
|
identity := biz.DataScope{UserID: userID, AuthorityID: authorityID}
|
||||||
return biz.DataScope{All: true}, nil
|
var user userPO
|
||||||
}
|
_ = r.data.gormDB.WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error
|
||||||
|
identity.PrimaryDeptID = user.DeptID
|
||||||
var authority authorityPO
|
var authority authorityPO
|
||||||
if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", authorityID).Error; err != nil {
|
_ = r.data.gormDB.WithContext(ctx).Select("authority_id", "data_scope").First(&authority, "authority_id = ?", authorityID).Error
|
||||||
return biz.DataScope{}, err
|
identity.Scope = authority.DataScope
|
||||||
|
if identity.Scope == 0 {
|
||||||
|
identity.Scope = 1
|
||||||
}
|
}
|
||||||
if authority.DataScope == 1 {
|
identity.All = identity.Scope == 1
|
||||||
return biz.DataScope{All: true}, nil
|
if identity.Scope == 4 {
|
||||||
}
|
identity.OwnerUserID = userID
|
||||||
if authority.DataScope == 4 {
|
|
||||||
return biz.DataScope{OwnerUserID: userID}, nil
|
|
||||||
}
|
}
|
||||||
var ids []uint
|
var ids []uint
|
||||||
if authority.DataScope == 5 {
|
_ = r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error
|
||||||
var err error
|
selected := make(map[uint]bool, len(ids)+1)
|
||||||
ids, err = r.DataScopeDepartmentIDs(ctx, authorityID)
|
for _, id := range ids {
|
||||||
if err != nil {
|
selected[id] = true
|
||||||
return biz.DataScope{}, err
|
}
|
||||||
}
|
if user.DeptID != 0 {
|
||||||
} else {
|
selected[user.DeptID] = true
|
||||||
if err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_user_id = ?", userID).Pluck("sys_department_id", &ids).Error; err != nil {
|
}
|
||||||
return biz.DataScope{}, err
|
ids = ids[:0]
|
||||||
}
|
for id := range selected {
|
||||||
if authority.DataScope == 2 && len(ids) > 0 {
|
ids = append(ids, id)
|
||||||
var departments []departmentPO
|
}
|
||||||
if err := r.data.gormDB.WithContext(ctx).Find(&departments).Error; err != nil {
|
if identity.Scope == 2 && len(ids) > 0 {
|
||||||
return biz.DataScope{}, err
|
var departments []departmentPO
|
||||||
}
|
_ = r.data.gormDB.WithContext(ctx).Find(&departments).Error
|
||||||
selected := make(map[uint]bool, len(ids))
|
for _, department := range departments {
|
||||||
for _, id := range ids {
|
for _, part := range strings.Split(department.Ancestors, ",") {
|
||||||
selected[id] = true
|
value, _ := strconv.ParseUint(part, 10, 64)
|
||||||
}
|
if selected[uint(value)] {
|
||||||
for _, department := range departments {
|
selected[department.ID] = true
|
||||||
for _, part := range strings.Split(department.Ancestors, ",") {
|
break
|
||||||
value, _ := strconv.ParseUint(part, 10, 64)
|
|
||||||
if selected[uint(value)] {
|
|
||||||
selected[department.ID] = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ids = ids[:0]
|
|
||||||
for id := range selected {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ids = ids[:0]
|
||||||
|
for id := range selected {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
} else if identity.Scope == 5 {
|
||||||
|
ids, _ = r.DataScopeDepartmentIDs(ctx, authorityID)
|
||||||
}
|
}
|
||||||
return biz.DataScope{DepartmentIDs: ids}, nil
|
identity.DepartmentIDs = ids
|
||||||
|
return identity, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ func NewCache(data *Data) biz.Cache {
|
||||||
return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)}
|
return &cacheStore{data: data, memory: make(map[string]memoryCacheEntry)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *cacheStore) client() *redis.Client {
|
func (s *cacheStore) client() redis.UniversalClient {
|
||||||
return s.data.redis.load()
|
return s.data.redis.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,14 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
|
useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
|
||||||
candidateRedis := openRedis(next.Data.Redis, useRedis)
|
candidateRedis := openRedis(next.Data.Redis, useRedis)
|
||||||
|
useMongo := next.Admin.System != nil && next.Admin.System.UseMongo
|
||||||
|
candidateMongo, mongoErr := openMongo(next.Data.Mongo, useMongo)
|
||||||
|
mongoAccepted := false
|
||||||
|
defer func() {
|
||||||
|
if !mongoAccepted && candidateMongo != nil {
|
||||||
|
_ = candidateMongo.Disconnect(context.Background())
|
||||||
|
}
|
||||||
|
}()
|
||||||
candidateDBList, err := openDatabaseList(next.Data.DatabaseList)
|
candidateDBList, err := openDatabaseList(next.Data.DatabaseList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -168,6 +176,10 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||||
d.gormDB.replace(candidateDB)
|
d.gormDB.replace(candidateDB)
|
||||||
d.replaceDatabaseList(candidateDBList)
|
d.replaceDatabaseList(candidateDBList)
|
||||||
d.redis.replace(candidateRedis)
|
d.redis.replace(candidateRedis)
|
||||||
|
if mongoErr == nil {
|
||||||
|
d.mongo.replace(candidateMongo)
|
||||||
|
mongoAccepted = true
|
||||||
|
}
|
||||||
d.runtime.Replace(next.Data, next.Admin)
|
d.runtime.Replace(next.Data, next.Admin)
|
||||||
if d.storage != nil {
|
if d.storage != nil {
|
||||||
d.storage.replace(candidateStorage)
|
d.storage.replace(candidateStorage)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ type Data struct {
|
||||||
configMu sync.Mutex
|
configMu sync.Mutex
|
||||||
gormDB *reloadableDB
|
gormDB *reloadableDB
|
||||||
redis *reloadableRedis
|
redis *reloadableRedis
|
||||||
|
mongo *reloadableMongo
|
||||||
runtime *conf.Runtime
|
runtime *conf.Runtime
|
||||||
storage *reloadableStorage
|
storage *reloadableStorage
|
||||||
dbListMu sync.RWMutex
|
dbListMu sync.RWMutex
|
||||||
|
|
@ -41,6 +42,7 @@ func openDatabaseList(configs []*conf.Data_Database) (map[string]*gorm.DB, error
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("open database %q: %w", config.AliasName, err)
|
return nil, fmt.Errorf("open database %q: %w", config.AliasName, err)
|
||||||
}
|
}
|
||||||
|
registerDataScopeCallbacks(db)
|
||||||
items[config.AliasName] = db
|
items[config.AliasName] = db
|
||||||
}
|
}
|
||||||
return items, nil
|
return items, nil
|
||||||
|
|
@ -108,28 +110,45 @@ func NewData(runtime *conf.Runtime) (*Data, func(), error) {
|
||||||
}
|
}
|
||||||
useRedis := admin != nil && admin.System != nil && admin.System.UseRedis
|
useRedis := admin != nil && admin.System != nil && admin.System.UseRedis
|
||||||
d.redis = newReloadableRedis(openRedis(c.Redis, useRedis))
|
d.redis = newReloadableRedis(openRedis(c.Redis, useRedis))
|
||||||
|
useMongo := admin != nil && admin.System != nil && admin.System.UseMongo
|
||||||
|
mongoClient, err := openMongo(c.Mongo, useMongo)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("mongo unavailable: %v", err)
|
||||||
|
mongoClient = nil
|
||||||
|
}
|
||||||
|
d.mongo = newReloadableMongo(mongoClient)
|
||||||
stopConfigWatcher := d.watchConfig()
|
stopConfigWatcher := d.watchConfig()
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
stopConfigWatcher()
|
stopConfigWatcher()
|
||||||
d.gormDB.close()
|
d.gormDB.close()
|
||||||
closeDatabaseList(d.dbList)
|
closeDatabaseList(d.dbList)
|
||||||
d.redis.close()
|
d.redis.close()
|
||||||
|
d.mongo.close()
|
||||||
}
|
}
|
||||||
return d, cleanup, nil
|
return d, cleanup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func openRedis(config *conf.Data_Redis, enabled bool) *redis.Client {
|
func openRedis(config *conf.Data_Redis, enabled bool) redis.UniversalClient {
|
||||||
if !enabled || config == nil || config.Addr == "" {
|
if !enabled || config == nil || (config.Addr == "" && len(config.ClusterAddrs) == 0) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
options := &redis.Options{Addr: config.Addr, Network: config.Network}
|
var candidate redis.UniversalClient
|
||||||
if config.ReadTimeout != nil {
|
if config.UseCluster {
|
||||||
options.ReadTimeout = config.ReadTimeout.AsDuration()
|
addresses := config.ClusterAddrs
|
||||||
|
if len(addresses) == 0 && config.Addr != "" {
|
||||||
|
addresses = []string{config.Addr}
|
||||||
|
}
|
||||||
|
candidate = redis.NewClusterClient(&redis.ClusterOptions{Addrs: addresses, Password: config.Password})
|
||||||
|
} else {
|
||||||
|
options := &redis.Options{Addr: config.Addr, Network: config.Network, Password: config.Password, DB: int(config.Db)}
|
||||||
|
if config.ReadTimeout != nil {
|
||||||
|
options.ReadTimeout = config.ReadTimeout.AsDuration()
|
||||||
|
}
|
||||||
|
if config.WriteTimeout != nil {
|
||||||
|
options.WriteTimeout = config.WriteTimeout.AsDuration()
|
||||||
|
}
|
||||||
|
candidate = redis.NewClient(options)
|
||||||
}
|
}
|
||||||
if config.WriteTimeout != nil {
|
|
||||||
options.WriteTimeout = config.WriteTimeout.AsDuration()
|
|
||||||
}
|
|
||||||
candidate := redis.NewClient(options)
|
|
||||||
pingCtx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)
|
pingCtx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := candidate.Ping(pingCtx).Err(); err != nil {
|
if err := candidate.Ping(pingCtx).Err(); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"kra/internal/biz"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerDataScopeCallbacks installs the global GORM data-scope engine.
|
||||||
|
// System tables are deliberately excluded: their access is controlled by
|
||||||
|
// Casbin, while ownership columns on business tables are row-level scope.
|
||||||
|
func registerDataScopeCallbacks(db *gorm.DB) {
|
||||||
|
if db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := db.Callback().Query()
|
||||||
|
if q.Get("data_scope:query") == nil {
|
||||||
|
_ = q.Before("gorm:query").Register("data_scope:query", applyDataScope("query"))
|
||||||
|
}
|
||||||
|
u := db.Callback().Update()
|
||||||
|
if u.Get("data_scope:update") == nil {
|
||||||
|
_ = u.Before("gorm:update").Register("data_scope:update", applyDataScope("update"))
|
||||||
|
_ = u.Before("gorm:update").Register("data_scope:stamp_update", stampUpdatedBy)
|
||||||
|
_ = u.After("gorm:update").Register("data_scope:audit_update", auditBlockedWrite("update"))
|
||||||
|
}
|
||||||
|
d := db.Callback().Delete()
|
||||||
|
if d.Get("data_scope:delete") == nil {
|
||||||
|
_ = d.Before("gorm:delete").Register("data_scope:delete", applyDataScope("delete"))
|
||||||
|
_ = d.Before("gorm:delete").After("data_scope:delete").Register("data_scope:stamp_delete", stampDeletedBy)
|
||||||
|
_ = d.After("gorm:delete").Register("data_scope:audit_delete", auditBlockedWrite("delete"))
|
||||||
|
}
|
||||||
|
c := db.Callback().Create()
|
||||||
|
if c.Get("data_scope:stamp") == nil {
|
||||||
|
_ = c.Before("gorm:create").Register("data_scope:stamp", stampOwnership)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasScopeField(db *gorm.DB, name string) bool {
|
||||||
|
return db.Statement.Schema != nil && db.Statement.Schema.LookUpField(name) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isControlledTable(db *gorm.DB) bool {
|
||||||
|
return db.Statement.Schema != nil && !strings.HasPrefix(db.Statement.Table, "sys_") && (hasScopeField(db, "dept_id") || hasScopeField(db, "created_by"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyDataScope(operation string) func(*gorm.DB) {
|
||||||
|
return func(db *gorm.DB) {
|
||||||
|
if !isControlledTable(db) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, done := db.Statement.Clauses["data_scope:applied"]; done {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if skip, ok := db.Get("data_scope:skip"); ok {
|
||||||
|
if value, _ := skip.(bool); value {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("数据权限: 业务表[%s]访问无身份上下文, 已放行(待补 ctx / 或使用系统上下文)", db.Statement.Table)
|
||||||
|
recordDataScopeEvent(db, "no_identity", operation, "无身份上下文访问受控表, 已放行", biz.DataScope{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (operation == "update" || operation == "delete") && !db.AllowGlobalUpdate && !hasWriteConditions(db) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.Statement.Clauses["data_scope:applied"] = clause.Clause{}
|
||||||
|
if scope.All {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
table := db.Statement.Table
|
||||||
|
if scope.OwnerUserID != 0 && hasScopeField(db, "created_by") {
|
||||||
|
db.Where(table+".created_by = ?", scope.OwnerUserID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if hasScopeField(db, "dept_id") {
|
||||||
|
ids := scope.DepartmentIDs
|
||||||
|
if len(ids) == 0 {
|
||||||
|
ids = []uint{0}
|
||||||
|
}
|
||||||
|
db.Where(table+".dept_id IN ?", ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordDataScopeEvent(db *gorm.DB, eventType, operation, detail string, scope biz.DataScope) {
|
||||||
|
record := &dataAccessLogPO{EventType: eventType, TargetTable: db.Statement.Table, Operation: operation, UserID: scope.UserID, AuthorityID: scope.AuthorityID, Scope: scope.Scope, RequestID: scope.RequestID, Method: scope.Method, Path: scope.Path, Detail: detail}
|
||||||
|
_ = db.Session(&gorm.Session{NewDB: true, SkipHooks: true}).Create(record).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func auditBlockedWrite(operation string) func(*gorm.DB) {
|
||||||
|
return func(db *gorm.DB) {
|
||||||
|
if _, applied := db.Statement.Clauses["data_scope:applied"]; !applied || db.Error != nil || db.RowsAffected != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if scope, ok := biz.DataScopeFromContext(db.Statement.Context); ok && !scope.All {
|
||||||
|
recordDataScopeEvent(db, "blocked_write", operation, "数据范围过滤后写操作影响 0 行(疑似越权尝试)", scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stampOwnership(db *gorm.DB) {
|
||||||
|
if !isControlledTable(db) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if hasScopeField(db, "created_by") && scope.UserID != 0 {
|
||||||
|
db.Statement.SetColumn("created_by", scope.UserID, true)
|
||||||
|
}
|
||||||
|
if hasScopeField(db, "dept_id") && scope.PrimaryDeptID != 0 {
|
||||||
|
db.Statement.SetColumn("dept_id", scope.PrimaryDeptID, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stampUpdatedBy(db *gorm.DB) {
|
||||||
|
stmt := db.Statement
|
||||||
|
if !isControlledTable(db) || stmt.SkipHooks || !hasScopeField(db, "updated_by") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
|
||||||
|
if !ok || scope.UserID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, isMap := stmt.Dest.(map[string]interface{}); !isMap {
|
||||||
|
if _, isMaps := stmt.Dest.([]map[string]interface{}); !isMaps && stmt.ReflectValue.Kind() == reflect.Struct && !stmt.ReflectValue.CanAddr() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, omitted := range stmt.Omits {
|
||||||
|
if omitted == "updated_by" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(stmt.Selects) > 0 {
|
||||||
|
found := false
|
||||||
|
for _, selected := range stmt.Selects {
|
||||||
|
if selected == "updated_by" || selected == "*" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
stmt.Selects = append(stmt.Selects, "updated_by")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stmt.SetColumn("updated_by", scope.UserID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stampDeletedBy(db *gorm.DB) {
|
||||||
|
stmt := db.Statement
|
||||||
|
if db.Error != nil || !isControlledTable(db) || stmt.SQL.Len() != 0 || stmt.Unscoped || !hasScopeField(db, "deleted_by") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deletedAtType := reflect.TypeOf(gorm.DeletedAt{})
|
||||||
|
var deletedAt *schema.Field
|
||||||
|
for _, field := range stmt.Schema.Fields {
|
||||||
|
if field.FieldType == deletedAtType || field.IndirectFieldType == deletedAtType {
|
||||||
|
deletedAt = field
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if deletedAt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, customZero := deletedAt.TagSettings["ZEROVALUE"]; customZero {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope, ok := biz.DataScopeFromContext(stmt.Context)
|
||||||
|
if !ok || scope.UserID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var primaryExpressions []clause.Expression
|
||||||
|
_, queryValues := schema.GetIdentityFieldValuesMap(stmt.Context, stmt.ReflectValue, stmt.Schema.PrimaryFields)
|
||||||
|
column, values := schema.ToQueryValues(stmt.Table, stmt.Schema.PrimaryFieldDBNames, queryValues)
|
||||||
|
if len(values) > 0 {
|
||||||
|
primaryExpressions = append(primaryExpressions, clause.IN{Column: column, Values: values})
|
||||||
|
}
|
||||||
|
if stmt.ReflectValue.CanAddr() && stmt.Dest != stmt.Model && stmt.Model != nil {
|
||||||
|
_, queryValues = schema.GetIdentityFieldValuesMap(stmt.Context, reflect.ValueOf(stmt.Model), stmt.Schema.PrimaryFields)
|
||||||
|
column, values = schema.ToQueryValues(stmt.Table, stmt.Schema.PrimaryFieldDBNames, queryValues)
|
||||||
|
if len(values) > 0 {
|
||||||
|
primaryExpressions = append(primaryExpressions, clause.IN{Column: column, Values: values})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, hasWhere := stmt.Clauses["WHERE"]; !hasWhere && len(primaryExpressions) == 0 && !db.AllowGlobalUpdate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := db.NowFunc()
|
||||||
|
stmt.AddClause(clause.Set{{Column: clause.Column{Name: deletedAt.DBName}, Value: now}, {Column: clause.Column{Name: "deleted_by"}, Value: scope.UserID}})
|
||||||
|
stmt.SetColumn(deletedAt.DBName, now, true)
|
||||||
|
stmt.SetColumn("deleted_by", scope.UserID, true)
|
||||||
|
if len(primaryExpressions) > 0 {
|
||||||
|
stmt.AddClause(clause.Where{Exprs: primaryExpressions})
|
||||||
|
}
|
||||||
|
gorm.SoftDeleteQueryClause{ZeroValue: sql.NullString{Valid: false}, Field: deletedAt}.ModifyStatement(stmt)
|
||||||
|
stmt.AddClauseIfNotExists(clause.Update{})
|
||||||
|
stmt.Build(stmt.DB.Callback().Update().Clauses...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasWriteConditions(db *gorm.DB) bool {
|
||||||
|
if value, ok := db.Statement.Clauses["WHERE"]; ok {
|
||||||
|
if where, ok := value.Expression.(clause.Where); ok && len(where.Exprs) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if db.Statement.Schema == nil || !db.Statement.ReflectValue.IsValid() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, values := schema.GetIdentityFieldValuesMap(db.Statement.Context, db.Statement.ReflectValue, db.Statement.Schema.PrimaryFields)
|
||||||
|
if _, query := schema.ToQueryValues(db.Statement.Table, db.Statement.Schema.PrimaryFieldDBNames, values); len(query) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if db.Statement.ReflectValue.CanAddr() && db.Statement.Dest != db.Statement.Model && db.Statement.Model != nil {
|
||||||
|
_, values = schema.GetIdentityFieldValuesMap(db.Statement.Context, reflect.ValueOf(db.Statement.Model), db.Statement.Schema.PrimaryFields)
|
||||||
|
_, query := schema.ToQueryValues(db.Statement.Table, db.Statement.Schema.PrimaryFieldDBNames, values)
|
||||||
|
return len(query) > 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
oracle "github.com/dzwvip/gorm-oracle"
|
oracle "github.com/dzwvip/gorm-oracle"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
|
|
@ -15,6 +16,8 @@ import (
|
||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/driver/sqlserver"
|
"gorm.io/driver/sqlserver"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -99,21 +102,61 @@ func databaseDSN(c *conf.Data_Database, name string) (string, error) {
|
||||||
return "", fmt.Errorf("unsupported database driver %q", c.Driver)
|
return "", fmt.Errorf("unsupported database driver %q", c.Driver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gormConfig(config *conf.Data_Database) *gorm.Config {
|
||||||
|
level := logger.Info
|
||||||
|
switch strings.ToLower(config.LogMode) {
|
||||||
|
case "silent":
|
||||||
|
level = logger.Silent
|
||||||
|
case "error":
|
||||||
|
level = logger.Error
|
||||||
|
case "warn":
|
||||||
|
level = logger.Warn
|
||||||
|
}
|
||||||
|
return &gorm.Config{Logger: logger.Default.LogMode(level), NamingStrategy: schema.NamingStrategy{TablePrefix: config.Prefix, SingularTable: config.Singular}}
|
||||||
|
}
|
||||||
|
|
||||||
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
||||||
|
return openWithDriverConfig(driver, dsn, &conf.Data_Database{Driver: driver})
|
||||||
|
}
|
||||||
|
|
||||||
|
func openWithDriverConfig(driver, dsn string, config *conf.Data_Database) (*gorm.DB, error) {
|
||||||
|
gormConfig := gormConfig(config)
|
||||||
|
var db *gorm.DB
|
||||||
|
var err error
|
||||||
switch normalizedDriver(driver) {
|
switch normalizedDriver(driver) {
|
||||||
case "mysql":
|
case "mysql":
|
||||||
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
db, err = gorm.Open(mysql.Open(dsn), gormConfig)
|
||||||
case "pgsql":
|
case "pgsql":
|
||||||
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
db, err = gorm.Open(postgres.Open(dsn), gormConfig)
|
||||||
case "mssql":
|
case "mssql":
|
||||||
return gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
|
db, err = gorm.Open(sqlserver.Open(dsn), gormConfig)
|
||||||
case "oracle":
|
case "oracle":
|
||||||
return gorm.Open(oracle.Open(dsn), &gorm.Config{})
|
db, err = gorm.Open(oracle.Open(dsn), gormConfig)
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
db, err = gorm.Open(sqlite.Open(dsn), gormConfig)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported database driver %q", driver)
|
return nil, fmt.Errorf("unsupported database driver %q", driver)
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if config.MaxIdleConns > 0 {
|
||||||
|
sqlDB.SetMaxIdleConns(int(config.MaxIdleConns))
|
||||||
|
}
|
||||||
|
if config.MaxOpenConns > 0 {
|
||||||
|
sqlDB.SetMaxOpenConns(int(config.MaxOpenConns))
|
||||||
|
}
|
||||||
|
if config.ConnMaxLifetime > 0 {
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Duration(config.ConnMaxLifetime) * time.Second)
|
||||||
|
}
|
||||||
|
if config.Engine != "" && (normalizedDriver(driver) == "mysql" || normalizedDriver(driver) == "mssql") {
|
||||||
|
db = db.Set("gorm:table_options", "ENGINE="+config.Engine)
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB, error) {
|
func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB, error) {
|
||||||
|
|
@ -129,7 +172,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
|
||||||
if err = os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
|
if err = os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return openWithDriver(driver, dsn)
|
return openWithDriverConfig(driver, dsn, c)
|
||||||
}
|
}
|
||||||
if create && driver != "oracle" {
|
if create && driver != "oracle" {
|
||||||
if !databaseNamePattern.MatchString(c.Name) {
|
if !databaseNamePattern.MatchString(c.Name) {
|
||||||
|
|
@ -146,7 +189,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
adminDB, err := openWithDriver(driver, dsn)
|
adminDB, err := openWithDriverConfig(driver, dsn, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("connect database server: %w", err)
|
return nil, fmt.Errorf("connect database server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +225,7 @@ func openDatabase(c *conf.Data_Database, create bool, template string) (*gorm.DB
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return openWithDriver(driver, dsn)
|
return openWithDriverConfig(driver, dsn, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openFallbackDatabase() (*gorm.DB, error) {
|
func openFallbackDatabase() (*gorm.DB, error) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ type dictionaryPO struct {
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
Name string
|
Name string
|
||||||
Type string `gorm:"uniqueIndex"`
|
Type string
|
||||||
Status bool
|
Status bool
|
||||||
Desc string
|
Desc string
|
||||||
ParentID *uint
|
ParentID *uint
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,7 @@ func (r *emailRepo) DefaultRecipients() []string {
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
parts := strings.Split(config.To, ",")
|
return []string{config.To}
|
||||||
result := make([]string, 0, len(parts))
|
|
||||||
for _, part := range parts {
|
|
||||||
if recipient := strings.TrimSpace(part); recipient != "" {
|
|
||||||
result = append(result, recipient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanHeader(value string) string {
|
func cleanHeader(value string) string {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ type exportTemplatePO struct {
|
||||||
DBName string
|
DBName string
|
||||||
Name string
|
Name string
|
||||||
DBTableName string `gorm:"column:table_name"`
|
DBTableName string `gorm:"column:table_name"`
|
||||||
TemplateID string `gorm:"uniqueIndex"`
|
TemplateID string
|
||||||
TemplateInfo string `gorm:"type:text"`
|
TemplateInfo string `gorm:"type:text"`
|
||||||
SQL string `gorm:"type:text"`
|
SQL string `gorm:"type:text"`
|
||||||
ImportSQL string `gorm:"type:text"`
|
ImportSQL string `gorm:"type:text"`
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
func migrateAll(db *gorm.DB) error {
|
func migrateAll(db *gorm.DB) error {
|
||||||
return db.AutoMigrate(
|
if err := db.AutoMigrate(
|
||||||
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
|
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
|
||||||
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{},
|
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &menuButtonPO{}, &authorityButtonPO{},
|
||||||
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
||||||
|
|
@ -12,5 +16,66 @@ func migrateAll(db *gorm.DB) error {
|
||||||
&operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{},
|
&operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{},
|
||||||
&taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
|
&taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
|
||||||
&announcementPO{},
|
&announcementPO{},
|
||||||
)
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return reconcileReferenceIndexes(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconcileReferenceIndexes removes constraints created by older Kra builds
|
||||||
|
// that are not part of the administration data model. Business services own
|
||||||
|
// duplicate checks and their user-facing error messages.
|
||||||
|
func reconcileReferenceIndexes(db *gorm.DB) error {
|
||||||
|
obsolete := []struct {
|
||||||
|
model any
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{&apiPO{}, "idx_api_path_method"},
|
||||||
|
{&dictionaryPO{}, "idx_sys_dictionaries_type"},
|
||||||
|
{¶meterPO{}, "idx_sys_params_key"},
|
||||||
|
{&apiTokenPO{}, "idx_sys_api_tokens_token"},
|
||||||
|
{&exportTemplatePO{}, "idx_sys_export_templates_template_id"},
|
||||||
|
}
|
||||||
|
for _, item := range obsolete {
|
||||||
|
if db.Migrator().HasIndex(item.model, item.name) {
|
||||||
|
if err := db.Migrator().DropIndex(item.model, item.name); err != nil {
|
||||||
|
return fmt.Errorf("drop obsolete index %s: %w", item.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, item := range []struct {
|
||||||
|
name string
|
||||||
|
field string
|
||||||
|
}{{"idx_sys_users_uuid", "UUID"}, {"idx_sys_users_username", "Username"}} {
|
||||||
|
unique, err := indexIsUnique(db, &userPO{}, item.name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !unique {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = db.Migrator().DropIndex(&userPO{}, item.name); err != nil {
|
||||||
|
return fmt.Errorf("drop legacy unique index %s: %w", item.name, err)
|
||||||
|
}
|
||||||
|
if err = db.Migrator().CreateIndex(&userPO{}, item.field); err != nil {
|
||||||
|
return fmt.Errorf("create reference index %s: %w", item.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexIsUnique(db *gorm.DB, model any, name string) (bool, error) {
|
||||||
|
indexes, err := db.Migrator().GetIndexes(model)
|
||||||
|
if err != nil {
|
||||||
|
// Some third-party GORM drivers do not implement index inspection.
|
||||||
|
// Fresh schemas are already correct; skip only the legacy repair there.
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for _, index := range indexes {
|
||||||
|
if index.Name() == name {
|
||||||
|
unique, known := index.Unique()
|
||||||
|
return known && unique, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"kra/internal/conf"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mongoURI(config *conf.Data_Mongo) string {
|
||||||
|
hosts := make([]string, 0, len(config.Hosts))
|
||||||
|
for _, host := range config.Hosts {
|
||||||
|
if host != nil && host.Host != "" && host.Port != "" {
|
||||||
|
hosts = append(hosts, host.Host+":"+host.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uri := "mongodb://" + strings.Join(hosts, ",") + "/" + config.Database
|
||||||
|
if config.Options != "" {
|
||||||
|
uri += "?" + config.Options
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
func openMongo(config *conf.Data_Mongo, enabled bool) (*mongo.Client, error) {
|
||||||
|
if !enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if config == nil || len(config.Hosts) == 0 {
|
||||||
|
return nil, fmt.Errorf("mongo hosts are required")
|
||||||
|
}
|
||||||
|
clientOptions := options.Client().ApplyURI(mongoURI(config))
|
||||||
|
if config.Username != "" && config.Password != "" {
|
||||||
|
clientOptions.SetAuth(options.Credential{Username: config.Username, Password: config.Password, AuthSource: config.AuthSource})
|
||||||
|
}
|
||||||
|
if config.MinPoolSize > 0 {
|
||||||
|
clientOptions.SetMinPoolSize(config.MinPoolSize)
|
||||||
|
}
|
||||||
|
if config.MaxPoolSize > 0 {
|
||||||
|
clientOptions.SetMaxPoolSize(config.MaxPoolSize)
|
||||||
|
}
|
||||||
|
if config.ConnectTimeoutMs > 0 {
|
||||||
|
clientOptions.SetConnectTimeout(time.Duration(config.ConnectTimeoutMs) * time.Millisecond)
|
||||||
|
}
|
||||||
|
if config.SocketTimeoutMs > 0 {
|
||||||
|
clientOptions.SetSocketTimeout(time.Duration(config.SocketTimeoutMs) * time.Millisecond)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
client, err := mongo.Connect(ctx, clientOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = client.Ping(ctx, nil); err != nil {
|
||||||
|
_ = client.Disconnect(context.Background())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ type parameterPO struct {
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
Name string
|
Name string
|
||||||
Key string `gorm:"uniqueIndex"`
|
Key string
|
||||||
Value string
|
Value string
|
||||||
Desc string
|
Desc string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,8 +18,40 @@ type reloadableDB struct {
|
||||||
retired []*gorm.DB
|
retired []*gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type reloadableMongo struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
current *mongo.Client
|
||||||
|
retired []*mongo.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newReloadableMongo(client *mongo.Client) *reloadableMongo {
|
||||||
|
return &reloadableMongo{current: client}
|
||||||
|
}
|
||||||
|
func (r *reloadableMongo) replace(client *mongo.Client) {
|
||||||
|
r.mu.Lock()
|
||||||
|
old := r.current
|
||||||
|
r.current = client
|
||||||
|
if old != nil && old != client {
|
||||||
|
r.retired = append(r.retired, old)
|
||||||
|
}
|
||||||
|
r.mu.Unlock()
|
||||||
|
}
|
||||||
|
func (r *reloadableMongo) close() {
|
||||||
|
r.mu.Lock()
|
||||||
|
all := append([]*mongo.Client{r.current}, r.retired...)
|
||||||
|
r.current = nil
|
||||||
|
r.retired = nil
|
||||||
|
r.mu.Unlock()
|
||||||
|
for _, client := range all {
|
||||||
|
if client != nil {
|
||||||
|
_ = client.Disconnect(context.Background())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newReloadableDB(db *gorm.DB) *reloadableDB {
|
func newReloadableDB(db *gorm.DB) *reloadableDB {
|
||||||
r := &reloadableDB{}
|
r := &reloadableDB{}
|
||||||
|
registerDataScopeCallbacks(db)
|
||||||
r.current.Store(db)
|
r.current.Store(db)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +63,7 @@ func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB {
|
||||||
func (r *reloadableDB) DB() *gorm.DB { return r.current.Load() }
|
func (r *reloadableDB) DB() *gorm.DB { return r.current.Load() }
|
||||||
|
|
||||||
func (r *reloadableDB) replace(db *gorm.DB) {
|
func (r *reloadableDB) replace(db *gorm.DB) {
|
||||||
|
registerDataScopeCallbacks(db)
|
||||||
old := r.current.Swap(db)
|
old := r.current.Swap(db)
|
||||||
if old != nil && old != db {
|
if old != nil && old != db {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
|
|
@ -60,45 +94,41 @@ func (r *reloadableDB) close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type reloadableRedis struct {
|
type reloadableRedis struct {
|
||||||
current atomic.Pointer[redis.Client]
|
mu sync.RWMutex
|
||||||
mu sync.Mutex
|
current redis.UniversalClient
|
||||||
retired []*redis.Client
|
retired []redis.UniversalClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func newReloadableRedis(client *redis.Client) *reloadableRedis {
|
func newReloadableRedis(client redis.UniversalClient) *reloadableRedis {
|
||||||
r := &reloadableRedis{}
|
return &reloadableRedis{current: client}
|
||||||
if client != nil {
|
|
||||||
r.current.Store(client)
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *reloadableRedis) load() *redis.Client { return r.current.Load() }
|
func (r *reloadableRedis) load() redis.UniversalClient {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
return r.current
|
||||||
|
}
|
||||||
|
|
||||||
func (r *reloadableRedis) replace(client *redis.Client) {
|
func (r *reloadableRedis) replace(client redis.UniversalClient) {
|
||||||
old := r.current.Swap(client)
|
r.mu.Lock()
|
||||||
|
old := r.current
|
||||||
|
r.current = client
|
||||||
if old != nil && old != client {
|
if old != nil && old != client {
|
||||||
r.mu.Lock()
|
|
||||||
r.retired = append(r.retired, old)
|
r.retired = append(r.retired, old)
|
||||||
r.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
r.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *reloadableRedis) close() {
|
func (r *reloadableRedis) close() {
|
||||||
current := r.current.Load()
|
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
all := append([]*redis.Client{current}, r.retired...)
|
all := append([]redis.UniversalClient{r.current}, r.retired...)
|
||||||
|
r.current = nil
|
||||||
r.retired = nil
|
r.retired = nil
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
seen := map[*redis.Client]struct{}{}
|
|
||||||
for _, client := range all {
|
for _, client := range all {
|
||||||
if client == nil {
|
if client == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := seen[client]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[client] = struct{}{}
|
|
||||||
_ = client.Close()
|
_ = client.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ type userPO struct {
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
UUID string `gorm:"type:char(36);uniqueIndex"`
|
UUID string `gorm:"type:char(36);index"`
|
||||||
Username string `gorm:"index;uniqueIndex"`
|
Username string `gorm:"index"`
|
||||||
Password string
|
Password string
|
||||||
NickName string `gorm:"column:nick_name"`
|
NickName string `gorm:"column:nick_name"`
|
||||||
HeaderImg string `gorm:"column:header_img"`
|
HeaderImg string `gorm:"column:header_img"`
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,17 @@ func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, ignored := range []ignoredAPIPO{
|
||||||
|
{Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/health"},
|
||||||
|
{Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"},
|
||||||
|
{Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"},
|
||||||
|
{Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"},
|
||||||
|
{Method: "GET", Path: "/info/getInfoPublic"},
|
||||||
|
} {
|
||||||
|
if err := tx.FirstOrCreate(&ignored, ignored).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -204,11 +215,16 @@ func defaultMenus() []menuPO {
|
||||||
child := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
child := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
||||||
return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
|
return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
|
||||||
}
|
}
|
||||||
|
cachedChild := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
||||||
|
value := child(parent, path, name, component, title, icon, sort)
|
||||||
|
value.KeepAlive = true
|
||||||
|
return value
|
||||||
|
}
|
||||||
return []menuPO{
|
return []menuPO{
|
||||||
{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
|
{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
|
||||||
root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10),
|
root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10),
|
||||||
{Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
|
{Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
|
||||||
child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), child("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), child("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
|
child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), cachedChild("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), cachedChild("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
|
||||||
child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
|
child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
|
||||||
child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
|
child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
|
||||||
child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
|
child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,10 @@ import (
|
||||||
kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
|
kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewGinServer(c *conf.Server, runtime *conf.Runtime, system *service.SystemService, access *service.AccessService, authority *handler.Authority, menu *handler.Menu, api *handler.API, permission *handler.Permission, organization *handler.Organization, announcement *handler.Announcement, email *handler.Email, task *handler.Task, media *handler.Media, auditHandler *handler.Audit, export *handler.Export, version *handler.Version, dictionary *handler.Dictionary, parameter *handler.Parameter, apiToken *handler.APIToken, systemConfig *handler.SystemConfig, publicHandler *handler.Public, user *handler.User, navigation *handler.Navigation, session *handler.Session, settings *service.SettingsService, audit *service.AuditService, emails *service.EmailService, logger *slog.Logger) *kratoshttp.Server {
|
func NewGinServer(c *conf.Server, runtime *conf.Runtime, system *service.SystemService, access *service.AccessService, authority *handler.Authority, menu *handler.Menu, api *handler.API, permission *handler.Permission, organization *handler.Organization, announcement *handler.Announcement, email *handler.Email, task *handler.Task, media *handler.Media, auditHandler *handler.Audit, export *handler.Export, version *handler.Version, dictionary *handler.Dictionary, parameter *handler.Parameter, apiToken *handler.APIToken, systemConfig *handler.SystemConfig, publicHandler *handler.Public, user *handler.User, navigation *handler.Navigation, session *handler.Session, settings *service.SettingsService, audit *service.AuditService, logger *slog.Logger) *kratoshttp.Server {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
engine.Use(servermiddleware.RequestMeta(), servermiddleware.EmailErrorAlert(emails, logger), gin.Recovery(), servermiddleware.SecurityRateLimit(system, settings), servermiddleware.OperationAudit(audit))
|
engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(audit, logger), servermiddleware.AccessLog(runtime, logger), servermiddleware.ErrorAudit(audit), servermiddleware.SecurityRateLimit(system, settings), servermiddleware.OperationAudit(runtime, audit))
|
||||||
|
|
||||||
prefix := ""
|
prefix := ""
|
||||||
config := runtime.Admin()
|
config := runtime.Admin()
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,11 @@ func (h *Media) DeleteMany(c *gin.Context) {
|
||||||
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "批量删除成功")
|
||||||
}
|
}
|
||||||
func (h *Media) Find(c *gin.Context) {
|
func (h *Media) Find(c *gin.Context) {
|
||||||
id, _ := strconv.ParseUint(c.Query("id"), 10, 64)
|
id, parseErr := strconv.ParseUint(c.Query("id"), 10, 64)
|
||||||
|
if parseErr != nil {
|
||||||
|
httpx.Fail(c, "文件ID非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
item, err := h.service.Media(c.Request.Context(), uint(id))
|
item, err := h.service.Media(c.Request.Context(), uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpx.Fail(c, "查询失败")
|
httpx.Fail(c, "查询失败")
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,13 @@ func (h *Public) Login(c *gin.Context) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = h.system.CacheIncrement(c.Request.Context(), c.ClientIP(), ipTTL)
|
_, _ = h.system.CacheIncrement(c.Request.Context(), c.ClientIP(), ipTTL)
|
||||||
if errors.Is(err, biz.ErrUserDisabled) {
|
if errors.Is(err, biz.ErrUserDisabled) {
|
||||||
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户被禁止登录", Agent: c.Request.UserAgent()})
|
var disabled *service.UserDisabledError
|
||||||
|
errors.As(err, &disabled)
|
||||||
|
userID := uint(0)
|
||||||
|
if disabled != nil {
|
||||||
|
userID = disabled.UserID
|
||||||
|
}
|
||||||
|
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户被禁止登录", Agent: c.Request.UserAgent(), UserID: userID})
|
||||||
httpx.Fail(c, "用户被禁止登录")
|
httpx.Fail(c, "用户被禁止登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"kra/internal/biz"
|
"kra/internal/biz"
|
||||||
|
|
@ -33,13 +32,19 @@ func AccessControl(runtime *conf.Runtime, access *service.AccessService, audit *
|
||||||
requestID, _ := c.Get("request_id")
|
requestID, _ := c.Get("request_id")
|
||||||
requestIDText, _ := requestID.(string)
|
requestIDText, _ := requestID.(string)
|
||||||
_ = audit.RecordDataAccessRequest(c.Request.Context(), &dto.DataAccessRecordRequest{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
|
_ = audit.RecordDataAccessRequest(c.Request.Context(), &dto.DataAccessRecordRequest{EventType: "blocked_access", Operation: c.Request.Method, UserID: claims.ID, AuthorityID: claims.AuthorityID, RequestID: requestIDText, Method: c.Request.Method, Path: path, Detail: "Casbin policy denied the request"})
|
||||||
c.AbortWithStatusJSON(http.StatusForbidden, httpx.Response{Code: httpx.CodeError, Data: nil, Msg: "权限不足"})
|
httpx.Write(c, httpx.CodeError, gin.H{}, "权限不足")
|
||||||
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
|
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpx.Fail(c, "数据权限加载失败")
|
requestContext = c.Request.Context()
|
||||||
return
|
}
|
||||||
|
if scope, ok := biz.DataScopeFromContext(requestContext); ok {
|
||||||
|
requestID, _ := c.Get("request_id")
|
||||||
|
scope.RequestID, _ = requestID.(string)
|
||||||
|
scope.Method, scope.Path = c.Request.Method, path
|
||||||
|
requestContext = biz.NewDataScopeContext(requestContext, scope)
|
||||||
}
|
}
|
||||||
requestContext = biz.NewActorContext(requestContext, biz.Actor{UserID: claims.ID, AuthorityID: claims.AuthorityID})
|
requestContext = biz.NewActorContext(requestContext, biz.Actor{UserID: claims.ID, AuthorityID: claims.AuthorityID})
|
||||||
c.Request = c.Request.WithContext(requestContext)
|
c.Request = c.Request.WithContext(requestContext)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"kra/internal/conf"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccessLog is the single global request/response capture point, matching
|
||||||
|
// the reference middleware ordering and making every HTTP request observable.
|
||||||
|
func AccessLog(runtime *conf.Runtime, logger *slog.Logger) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var requestBody []byte
|
||||||
|
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
|
||||||
|
if c.Request.Body != nil && !multipart {
|
||||||
|
requestBody, _ = io.ReadAll(c.Request.Body)
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||||
|
}
|
||||||
|
maxBytes := 1 << 20
|
||||||
|
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
|
||||||
|
c.Writer = writer
|
||||||
|
started := time.Now()
|
||||||
|
c.Next()
|
||||||
|
if logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestText, responseText := "", ""
|
||||||
|
config := runtime.Admin()
|
||||||
|
logLimit := 32768
|
||||||
|
if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
|
||||||
|
logLimit = int(config.Zap.AccessLogMaxBytes)
|
||||||
|
}
|
||||||
|
if config == nil || config.Zap == nil || config.Zap.AccessReqBody {
|
||||||
|
if multipart {
|
||||||
|
requestText = "[文件]"
|
||||||
|
} else {
|
||||||
|
requestText = redactJSONLimit(requestBody, logLimit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config == nil || config.Zap == nil || config.Zap.AccessRespData {
|
||||||
|
responseText = redactJSONLimit(writer.body.Bytes(), logLimit)
|
||||||
|
}
|
||||||
|
userID, authorityID := uint(0), uint(0)
|
||||||
|
if claims := Claims(c); claims != nil {
|
||||||
|
userID, authorityID = claims.ID, claims.AuthorityID
|
||||||
|
}
|
||||||
|
route := c.FullPath()
|
||||||
|
if route == "" {
|
||||||
|
route = "unmatched"
|
||||||
|
}
|
||||||
|
attributes := []any{
|
||||||
|
"ip", c.ClientIP(), "method", c.Request.Method, "path", c.Request.URL.Path, "http_route", route,
|
||||||
|
"status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(),
|
||||||
|
"request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"),
|
||||||
|
"bytes_in", len(requestBody), "bytes_out", c.Writer.Size(), "user_id", userID, "authority_id", authorityID,
|
||||||
|
"ua", c.Request.UserAgent(), "query", c.Request.URL.RawQuery, "request", requestText, "response", responseText}
|
||||||
|
if config != nil && config.Zap != nil && config.Zap.AccessReqHeaders {
|
||||||
|
attributes = append(attributes, "headers", redactHeaders(c.Request.Header))
|
||||||
|
}
|
||||||
|
logger.InfoContext(c.Request.Context(), "http access", attributes...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactHeaders(headers map[string][]string) map[string][]string {
|
||||||
|
out := make(map[string][]string, len(headers))
|
||||||
|
for key, values := range headers {
|
||||||
|
lower := strings.ToLower(key)
|
||||||
|
if strings.Contains(lower, "token") || lower == "authorization" || lower == "cookie" {
|
||||||
|
out[key] = []string{"******"}
|
||||||
|
} else {
|
||||||
|
out[key] = values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringValueFromContext(c *gin.Context, key string) string {
|
||||||
|
value, _ := c.Get(key)
|
||||||
|
return stringValue(value)
|
||||||
|
}
|
||||||
|
|
@ -6,29 +6,34 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"kra/internal/conf"
|
||||||
"kra/internal/service"
|
"kra/internal/service"
|
||||||
"kra/internal/service/dto"
|
"kra/internal/service/dto"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func OperationAudit(service *service.AuditService) gin.HandlerFunc {
|
func OperationAudit(runtime *conf.Runtime, service *service.AuditService) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if isBootstrapPath(c.Request.URL.Path) || c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
if strings.Contains(path, "sysOperationRecord") || strings.Contains(path, "sysLoginLog") || strings.Contains(path, "dataAccessLog") {
|
if !recordsOperation(c.Request.Method, path) {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var requestBody []byte
|
var requestBody []byte
|
||||||
if c.Request.Body != nil {
|
maxBytes := 32768
|
||||||
requestBody, _ = io.ReadAll(io.LimitReader(c.Request.Body, 32769))
|
if config := runtime.Admin(); config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
|
||||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
maxBytes = int(config.Zap.AccessLogMaxBytes)
|
||||||
}
|
}
|
||||||
writer := &captureWriter{ResponseWriter: c.Writer}
|
if c.Request.Body != nil {
|
||||||
|
if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
|
||||||
|
requestBody = []byte("[文件]")
|
||||||
|
} else {
|
||||||
|
requestBody, _ = io.ReadAll(c.Request.Body)
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
|
||||||
c.Writer = writer
|
c.Writer = writer
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
@ -41,6 +46,49 @@ func OperationAudit(service *service.AuditService) gin.HandlerFunc {
|
||||||
if status >= 400 {
|
if status >= 400 {
|
||||||
errorMessage = writer.body.String()
|
errorMessage = writer.body.String()
|
||||||
}
|
}
|
||||||
_ = service.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: redactJSON(requestBody), Response: redactJSON(writer.body.Bytes()), UserID: userID, RequestID: stringValue(requestID), TraceID: c.GetHeader("traceparent"), DeviceID: c.GetHeader("X-Device-Id")})
|
_ = service.RecordOperationRequest(c.Request.Context(), &dto.OperationRecordRequest{IP: c.ClientIP(), Method: c.Request.Method, Path: path, Status: status, LatencyMS: time.Since(started).Milliseconds(), Agent: c.Request.UserAgent(), ErrorMessage: errorMessage, Body: redactJSONLimit(requestBody, maxBytes), Response: writer.body.String(), UserID: userID, RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), DeviceID: c.GetHeader("X-Device-Id")})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordsOperation mirrors the routes on which operation records are enabled.
|
||||||
|
// Matching by suffix keeps the behavior stable when router-prefix is configured.
|
||||||
|
func recordsOperation(method, path string) bool {
|
||||||
|
_, ok := operationRoutes[method+" "+routeSuffix(path)]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func routeSuffix(path string) string {
|
||||||
|
for _, marker := range []string{"/user/", "/api/", "/casbin/", "/authority/", "/menu/", "/department/", "/position/", "/sysDictionary/", "/sysDictionaryDetail/", "/sysParams/", "/securityConfig/", "/system/", "/sysApiToken/", "/sysVersion/", "/sysExportTemplate/", "/sysError/", "/sysLoginLog/", "/sysOperationRecord/", "/dataAccessLog/", "/timedTask/", "/info/", "/email/"} {
|
||||||
|
if index := strings.Index(path, marker); index >= 0 {
|
||||||
|
return path[index:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
var operationRoutes = func() map[string]struct{} {
|
||||||
|
values := []string{
|
||||||
|
"POST /user/admin_register", "POST /user/changePassword", "POST /user/setUserAuthority", "DELETE /user/deleteUser", "PUT /user/setUserInfo", "PUT /user/setSelfInfo", "POST /user/setUserAuthorities", "POST /user/setUserDepartments", "POST /user/setUserPositions", "POST /user/resetPassword", "PUT /user/setSelfSetting",
|
||||||
|
"GET /api/getApiGroups", "GET /api/syncApi", "POST /api/ignoreApi", "POST /api/enterSyncApi", "POST /api/createApi", "POST /api/deleteApi", "POST /api/getApiById", "POST /api/updateApi", "DELETE /api/deleteApisByIds", "POST /api/setApiRoles", "POST /casbin/updateCasbin",
|
||||||
|
"POST /authority/createAuthority", "POST /authority/deleteAuthority", "PUT /authority/updateAuthority", "POST /authority/copyAuthority", "POST /authority/setDataScope", "POST /authority/setRoleUsers",
|
||||||
|
"POST /menu/addBaseMenu", "POST /menu/addMenuAuthority", "POST /menu/deleteBaseMenu", "POST /menu/updateBaseMenu", "POST /menu/setMenuRoles",
|
||||||
|
"POST /department/createDepartment", "PUT /department/updateDepartment", "DELETE /department/deleteDepartment", "POST /department/setDepartmentUsers",
|
||||||
|
"POST /position/createPosition", "PUT /position/updatePosition", "DELETE /position/deletePosition", "POST /position/setPositionUsers",
|
||||||
|
"POST /sysDictionary/createSysDictionary", "DELETE /sysDictionary/deleteSysDictionary", "PUT /sysDictionary/updateSysDictionary", "POST /sysDictionary/importSysDictionary", "GET /sysDictionary/exportSysDictionary",
|
||||||
|
"POST /sysDictionaryDetail/createSysDictionaryDetail", "DELETE /sysDictionaryDetail/deleteSysDictionaryDetail", "PUT /sysDictionaryDetail/updateSysDictionaryDetail",
|
||||||
|
"POST /sysParams/createSysParams", "DELETE /sysParams/deleteSysParams", "DELETE /sysParams/deleteSysParamsByIds", "PUT /sysParams/updateSysParams",
|
||||||
|
"POST /securityConfig/setSecurityConfig", "POST /system/setSystemConfig", "POST /system/reloadSystem",
|
||||||
|
"POST /sysApiToken/createApiToken", "POST /sysApiToken/getApiTokenList", "POST /sysApiToken/deleteApiToken",
|
||||||
|
"DELETE /sysVersion/deleteSysVersion", "DELETE /sysVersion/deleteSysVersionByIds", "POST /sysVersion/exportVersion", "POST /sysVersion/importVersion",
|
||||||
|
"POST /sysExportTemplate/createSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplate", "DELETE /sysExportTemplate/deleteSysExportTemplateByIds", "PUT /sysExportTemplate/updateSysExportTemplate", "POST /sysExportTemplate/importExcel",
|
||||||
|
"DELETE /sysError/deleteSysError", "DELETE /sysError/deleteSysErrorByIds", "PUT /sysError/updateSysError",
|
||||||
|
"DELETE /sysLoginLog/deleteLoginLog", "DELETE /sysLoginLog/deleteLoginLogByIds", "DELETE /sysOperationRecord/deleteSysOperationRecord", "DELETE /sysOperationRecord/deleteSysOperationRecordByIds", "DELETE /dataAccessLog/deleteDataAccessLogByIds",
|
||||||
|
"POST /timedTask/createTimedTask", "PUT /timedTask/updateTimedTask", "DELETE /timedTask/deleteTimedTask", "POST /timedTask/toggleTimedTask", "POST /timedTask/triggerTimedTask",
|
||||||
|
"POST /info/createInfo", "DELETE /info/deleteInfo", "DELETE /info/deleteInfoByIds", "PUT /info/updateInfo", "POST /email/emailTest", "POST /email/sendEmail",
|
||||||
|
}
|
||||||
|
out := make(map[string]struct{}, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
out[value] = struct{}{}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -12,16 +13,28 @@ import (
|
||||||
"kra/pkg/adminauth"
|
"kra/pkg/adminauth"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
)
|
)
|
||||||
|
|
||||||
const claimsKey = "admin_claims"
|
const claimsKey = "admin_claims"
|
||||||
|
|
||||||
|
var refreshTokens singleflight.Group
|
||||||
|
|
||||||
|
type refreshedToken struct {
|
||||||
|
token string
|
||||||
|
expiresAt int64
|
||||||
|
}
|
||||||
|
|
||||||
func Auth(runtime *conf.Runtime, settings *service.SettingsService) gin.HandlerFunc {
|
func Auth(runtime *conf.Runtime, settings *service.SettingsService) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
token := c.GetHeader("x-token")
|
token := c.GetHeader("x-token")
|
||||||
if token == "" {
|
if token == "" {
|
||||||
token, _ = c.Cookie("x-token")
|
token, _ = c.Cookie("x-token")
|
||||||
}
|
}
|
||||||
|
if token == "" {
|
||||||
|
httpx.NoAuth(c, "未登录或非法访问,请登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
secret := ""
|
secret := ""
|
||||||
config := runtime.Admin()
|
config := runtime.Admin()
|
||||||
if config != nil && config.Jwt != nil {
|
if config != nil && config.Jwt != nil {
|
||||||
|
|
@ -29,15 +42,24 @@ func Auth(runtime *conf.Runtime, settings *service.SettingsService) gin.HandlerF
|
||||||
}
|
}
|
||||||
claims, err := adminauth.Parse(token, secret)
|
claims, err := adminauth.Parse(token, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpx.NoAuth(c, "未登录或非法访问")
|
message := "无法处理此token"
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, adminauth.ErrTokenExpired):
|
||||||
|
message = "登录已过期,请重新登录"
|
||||||
|
case errors.Is(err, adminauth.ErrTokenMalformed):
|
||||||
|
message = "这不是一个token"
|
||||||
|
case errors.Is(err, adminauth.ErrTokenSignatureInvalid):
|
||||||
|
message = "无效签名"
|
||||||
|
case errors.Is(err, adminauth.ErrTokenNotValidYet):
|
||||||
|
message = "token尚未激活"
|
||||||
|
}
|
||||||
|
httpx.SetTokenCookie(c, "", -1)
|
||||||
|
httpx.NoAuth(c, message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if disabled, checkErr := settings.IsTokenDisabled(c.Request.Context(), token); checkErr != nil || disabled {
|
if disabled, checkErr := settings.IsTokenDisabled(c.Request.Context(), token); checkErr != nil || disabled {
|
||||||
httpx.NoAuth(c, "登录状态已失效")
|
httpx.SetTokenCookie(c, "", -1)
|
||||||
return
|
httpx.NoAuth(c, "您的帐户异地登陆或令牌失效")
|
||||||
}
|
|
||||||
if active, checkErr := settings.ActiveTokenMatches(c.Request.Context(), claims.Username, token); checkErr != nil || !active {
|
|
||||||
httpx.NoAuth(c, "登录状态已失效")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if claims.ExpiresAt != nil && claims.BufferTime > 0 && time.Until(claims.ExpiresAt.Time) < time.Duration(claims.BufferTime)*time.Second {
|
if claims.ExpiresAt != nil && claims.BufferTime > 0 && time.Until(claims.ExpiresAt.Time) < time.Duration(claims.BufferTime)*time.Second {
|
||||||
|
|
@ -55,11 +77,21 @@ func Auth(runtime *conf.Runtime, settings *service.SettingsService) gin.HandlerF
|
||||||
issuer = config.Jwt.Issuer
|
issuer = config.Jwt.Issuer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newToken, newClaims, refreshErr := adminauth.Generate(secret, issuer, expires, buffer, claims.ID, claims.AuthorityID, claims.UUID, claims.Username, claims.NickName, claims.MustChangePwd)
|
value, refreshErr, _ := refreshTokens.Do(token, func() (any, error) {
|
||||||
if refreshErr == nil && settings.RotateActiveToken(c.Request.Context(), claims.Username, token, newToken, expires) == nil {
|
newToken, newClaims, generateErr := adminauth.Generate(secret, issuer, expires, buffer, claims.ID, claims.AuthorityID, claims.UUID, claims.Username, claims.NickName, claims.MustChangePwd)
|
||||||
c.Header("new-token", newToken)
|
if generateErr != nil {
|
||||||
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
|
return nil, generateErr
|
||||||
httpx.SetTokenCookie(c, newToken, int(expires.Seconds()))
|
}
|
||||||
|
if rotateErr := settings.RotateActiveToken(c.Request.Context(), claims.Username, token, newToken, expires); rotateErr != nil {
|
||||||
|
return nil, rotateErr
|
||||||
|
}
|
||||||
|
return refreshedToken{token: newToken, expiresAt: newClaims.ExpiresAt.Unix()}, nil
|
||||||
|
})
|
||||||
|
if refreshErr == nil {
|
||||||
|
refreshed := value.(refreshedToken)
|
||||||
|
c.Header("new-token", refreshed.token)
|
||||||
|
c.Header("new-expires-at", strconv.FormatInt(refreshed.expiresAt, 10))
|
||||||
|
httpx.SetTokenCookie(c, refreshed.token, int(expires.Seconds()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.Set(claimsKey, claims)
|
c.Set(claimsKey, claims)
|
||||||
|
|
@ -85,6 +117,6 @@ func MustChangePassword() gin.HandlerFunc {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "请先修改初始密码"})
|
c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,17 @@ import (
|
||||||
|
|
||||||
type captureWriter struct {
|
type captureWriter struct {
|
||||||
gin.ResponseWriter
|
gin.ResponseWriter
|
||||||
body bytes.Buffer
|
body bytes.Buffer
|
||||||
|
maxBytes int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *captureWriter) Write(data []byte) (int, error) {
|
func (w *captureWriter) Write(data []byte) (int, error) {
|
||||||
if w.body.Len() < 32768 {
|
limit := w.maxBytes
|
||||||
remaining := 32768 - w.body.Len()
|
if limit <= 0 {
|
||||||
|
limit = 32768
|
||||||
|
}
|
||||||
|
if w.body.Len() < limit {
|
||||||
|
remaining := limit - w.body.Len()
|
||||||
if len(data) > remaining {
|
if len(data) > remaining {
|
||||||
_, _ = w.body.Write(data[:remaining])
|
_, _ = w.body.Write(data[:remaining])
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -26,11 +31,18 @@ func (w *captureWriter) Write(data []byte) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func redactJSON(raw []byte) string {
|
func redactJSON(raw []byte) string {
|
||||||
|
return redactJSONLimit(raw, 32768)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactJSONLimit(raw []byte, limit int) string {
|
||||||
if len(raw) == 0 {
|
if len(raw) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if len(raw) > 32768 {
|
if limit <= 0 {
|
||||||
raw = raw[:32768]
|
limit = 32768
|
||||||
|
}
|
||||||
|
if len(raw) > limit {
|
||||||
|
raw = raw[:limit]
|
||||||
}
|
}
|
||||||
var value any
|
var value any
|
||||||
if json.Unmarshal(raw, &value) != nil {
|
if json.Unmarshal(raw, &value) != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"kra/internal/server/httpx"
|
|
||||||
"kra/internal/service"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func EmailErrorAlert(service *service.EmailService, logger *slog.Logger) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
if strings.Contains(c.Request.URL.Path, "/email/") {
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writer := &captureWriter{ResponseWriter: c.Writer}
|
|
||||||
c.Writer = writer
|
|
||||||
started := time.Now()
|
|
||||||
c.Next()
|
|
||||||
failed := c.Writer.Status() >= 400
|
|
||||||
if !failed {
|
|
||||||
var result struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
}
|
|
||||||
failed = json.Unmarshal(writer.body.Bytes(), &result) == nil && result.Code != httpx.CodeSuccess
|
|
||||||
}
|
|
||||||
if !failed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
username := "Unknown"
|
|
||||||
if claims := Claims(c); claims != nil && claims.Username != "" {
|
|
||||||
username = claims.Username
|
|
||||||
}
|
|
||||||
subject := fmt.Sprintf("%s %s 调用 %s 报错", username, c.ClientIP(), c.Request.URL.Path)
|
|
||||||
body := bytes.NewBuffer(nil)
|
|
||||||
fmt.Fprintf(body, "请求方式:%s<br>请求路径:%s<br>状态码:%d<br>耗时:%s<br>错误响应:<pre>%s</pre>", c.Request.Method, c.Request.URL.Path, c.Writer.Status(), time.Since(started), redactJSON(writer.body.Bytes()))
|
|
||||||
if err := service.Alert(c.Request.Context(), subject, body.String()); err != nil && logger != nil {
|
|
||||||
logger.Error("send HTTP error email", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"kra/internal/server/httpx"
|
||||||
|
"kra/internal/service"
|
||||||
|
"kra/internal/service/dto"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrorAudit supplies the database sink that the Error-level logging core
|
||||||
|
// provides. Expected authentication, permission and input failures are not
|
||||||
|
// system errors and therefore are not inserted into sys_error.
|
||||||
|
func ErrorAudit(audit *service.AuditService) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: 1 << 20}
|
||||||
|
c.Writer = writer
|
||||||
|
c.Next()
|
||||||
|
if strings.Contains(c.Request.URL.Path, "/sysError/") || c.Writer.Status() >= 300 && c.Writer.Status() < 500 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var response httpx.Response
|
||||||
|
if json.Unmarshal(writer.body.Bytes(), &response) != nil || response.Code == httpx.CodeSuccess || expectedClientFailure(response.Msg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestID, _ := c.Get("request_id")
|
||||||
|
_ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: response.Msg, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), Status: "未解决"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectedClientFailure(message string) bool {
|
||||||
|
for _, value := range []string{"参数错误", "请输入用户名和密码", "验证码错误", "用户名不存在或者密码错误", "用户被禁止登录", "账号已锁定", "权限不足", "密码已过期", "未登录", "token", "令牌失效"} {
|
||||||
|
if strings.Contains(message, value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"kra/internal/server/httpx"
|
"kra/internal/server/httpx"
|
||||||
|
|
@ -11,7 +12,8 @@ import (
|
||||||
|
|
||||||
func SecurityRateLimit(system *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
|
func SecurityRateLimit(system *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if isBootstrapPath(c.Request.URL.Path) {
|
path := strings.TrimSuffix(c.Request.URL.Path, "/")
|
||||||
|
if !strings.HasSuffix(path, "/base/login") && !strings.HasSuffix(path, "/base/captcha") {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"kra/internal/service"
|
||||||
|
"kra/internal/service/dto"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Recovery(audit *service.AuditService, logger *slog.Logger) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
defer func() {
|
||||||
|
panicValue := recover()
|
||||||
|
if panicValue == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
brokenPipe := false
|
||||||
|
if networkError, ok := panicValue.(*net.OpError); ok {
|
||||||
|
if syscallError, ok := networkError.Err.(*os.SyscallError); ok {
|
||||||
|
message := strings.ToLower(syscallError.Error())
|
||||||
|
brokenPipe = strings.Contains(message, "broken pipe") || strings.Contains(message, "connection reset by peer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request, _ := httputil.DumpRequest(c.Request, false)
|
||||||
|
info := fmt.Sprintf("error=%v request=%s stack=%s", panicValue, request, debug.Stack())
|
||||||
|
if logger != nil {
|
||||||
|
logger.ErrorContext(c.Request.Context(), "recovery from panic", "error", panicValue, "request", string(request), "stack", string(debug.Stack()))
|
||||||
|
}
|
||||||
|
requestID, _ := c.Get("request_id")
|
||||||
|
_ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: info, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id"), Status: "未解决"})
|
||||||
|
if brokenPipe {
|
||||||
|
if err, ok := panicValue.(error); ok {
|
||||||
|
_ = c.Error(err)
|
||||||
|
}
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
}()
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,48 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var traceParentPattern = regexp.MustCompile(`^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$`)
|
||||||
|
|
||||||
|
func randomHex(bytes int) string {
|
||||||
|
value := make([]byte, bytes)
|
||||||
|
_, _ = rand.Read(value)
|
||||||
|
return hex.EncodeToString(value)
|
||||||
|
}
|
||||||
|
|
||||||
func RequestMeta() gin.HandlerFunc {
|
func RequestMeta() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
requestID := c.GetHeader("X-Request-Id")
|
requestID := c.GetHeader("X-Request-Id")
|
||||||
if requestID == "" {
|
if requestID == "" || len(requestID) > 128 || strings.ContainsAny(requestID, "\r\n") {
|
||||||
requestID = uuid.NewString()
|
requestID = uuid.NewString()
|
||||||
}
|
}
|
||||||
|
traceID, parentSpanID := "", ""
|
||||||
|
if match := traceParentPattern.FindStringSubmatch(strings.ToLower(c.GetHeader("traceparent"))); len(match) == 3 {
|
||||||
|
traceID, parentSpanID = match[1], match[2]
|
||||||
|
} else if candidate := c.GetHeader("X-Trace-Id"); len(candidate) <= 128 && !strings.ContainsAny(candidate, "\r\n") {
|
||||||
|
traceID = candidate
|
||||||
|
}
|
||||||
|
if traceID == "" {
|
||||||
|
traceID = randomHex(16)
|
||||||
|
}
|
||||||
|
spanID := randomHex(8)
|
||||||
c.Header("X-Request-Id", requestID)
|
c.Header("X-Request-Id", requestID)
|
||||||
|
c.Header("X-Trace-Id", traceID)
|
||||||
|
if len(traceID) == 32 {
|
||||||
|
c.Header("traceparent", "00-"+traceID+"-"+spanID+"-01")
|
||||||
|
}
|
||||||
c.Set("request_id", requestID)
|
c.Set("request_id", requestID)
|
||||||
|
c.Set("trace_id", traceID)
|
||||||
|
c.Set("span_id", spanID)
|
||||||
|
c.Set("parent_span_id", parentSpanID)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,34 @@ package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"kra/internal/biz"
|
"kra/internal/biz"
|
||||||
|
"kra/internal/conf"
|
||||||
"kra/internal/service/dto"
|
"kra/internal/service/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccessService struct{ uc *biz.AccessUsecase }
|
type AccessService struct {
|
||||||
|
uc *biz.AccessUsecase
|
||||||
|
runtime *conf.Runtime
|
||||||
|
}
|
||||||
|
|
||||||
func NewAccessService(uc *biz.AccessUsecase) *AccessService { return &AccessService{uc: uc} }
|
func NewAccessService(uc *biz.AccessUsecase, runtime *conf.Runtime) *AccessService {
|
||||||
|
return &AccessService{uc: uc, runtime: runtime}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccessService) NormalizeRoutePath(path string) string {
|
||||||
|
config := s.runtime.Admin()
|
||||||
|
if config == nil || config.RouterPrefix == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
prefix := strings.TrimSuffix(config.RouterPrefix, "/")
|
||||||
|
normalized := strings.TrimPrefix(path, prefix)
|
||||||
|
if normalized == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
func (s *AccessService) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
|
func (s *AccessService) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) {
|
||||||
return s.uc.Authorize(ctx, aid, path, method)
|
return s.uc.Authorize(ctx, aid, path, method)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,16 @@ func (s *AccessService) PolicyPathResponses(ctx context.Context, authorityID uin
|
||||||
func (s *AccessService) SyncAPIResponses(ctx context.Context, routes []dto.APIRequest) (*dto.APISyncResponse, error) {
|
func (s *AccessService) SyncAPIResponses(ctx context.Context, routes []dto.APIRequest) (*dto.APISyncResponse, error) {
|
||||||
items := make([]*biz.API, 0, len(routes))
|
items := make([]*biz.API, 0, len(routes))
|
||||||
for i := range routes {
|
for i := range routes {
|
||||||
|
routes[i].Path = s.NormalizeRoutePath(routes[i].Path)
|
||||||
|
if routes[i].APIGroup == "" || routes[i].Description == "" {
|
||||||
|
group, description := routeMetadata(routes[i].Method, routes[i].Path)
|
||||||
|
if routes[i].APIGroup == "" {
|
||||||
|
routes[i].APIGroup = group
|
||||||
|
}
|
||||||
|
if routes[i].Description == "" {
|
||||||
|
routes[i].Description = description
|
||||||
|
}
|
||||||
|
}
|
||||||
items = append(items, apiDomain(&routes[i]))
|
items = append(items, apiDomain(&routes[i]))
|
||||||
}
|
}
|
||||||
return s.SyncAPIs(ctx, items)
|
return s.SyncAPIs(ctx, items)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type apiMetadataValue struct{ group, description string }
|
||||||
|
|
||||||
|
var apiMetadata = map[string]apiMetadataValue{
|
||||||
|
"DELETE /api/deleteApisByIds": {group: "api", description: "批量删除api"},
|
||||||
|
"DELETE /customer/customer": {group: "客户", description: "删除客户"},
|
||||||
|
"DELETE /dataAccessLog/deleteDataAccessLogByIds": {group: "数据权限审计", description: "批量删除数据权限审计日志"},
|
||||||
|
"DELETE /department/deleteDepartment": {group: "部门", description: "删除部门"},
|
||||||
|
"DELETE /info/deleteInfo": {group: "公告", description: "删除公告"},
|
||||||
|
"DELETE /info/deleteInfoByIds": {group: "公告", description: "批量删除公告"},
|
||||||
|
"DELETE /mediaUpload/:uploadId": {group: "媒体上传", description: "取消大文件上传"},
|
||||||
|
"DELETE /position/deletePosition": {group: "岗位", description: "删除岗位"},
|
||||||
|
"DELETE /sysDictionary/deleteSysDictionary": {group: "系统字典", description: "删除字典"},
|
||||||
|
"DELETE /sysDictionaryDetail/deleteSysDictionaryDetail": {group: "系统字典详情", description: "删除字典内容"},
|
||||||
|
"DELETE /sysError/deleteSysError": {group: "错误日志", description: "删除错误日志"},
|
||||||
|
"DELETE /sysError/deleteSysErrorByIds": {group: "错误日志", description: "批量删除错误日志"},
|
||||||
|
"DELETE /sysExportTemplate/deleteSysExportTemplate": {group: "导出模板", description: "删除导出模板"},
|
||||||
|
"DELETE /sysExportTemplate/deleteSysExportTemplateByIds": {group: "导出模板", description: "批量删除导出模板"},
|
||||||
|
"DELETE /sysLoginLog/deleteLoginLog": {group: "登录日志", description: "删除登录日志"},
|
||||||
|
"DELETE /sysLoginLog/deleteLoginLogByIds": {group: "登录日志", description: "批量删除登录日志"},
|
||||||
|
"DELETE /sysOperationRecord/deleteSysOperationRecord": {group: "操作记录", description: "删除操作记录"},
|
||||||
|
"DELETE /sysOperationRecord/deleteSysOperationRecordByIds": {group: "操作记录", description: "批量删除操作历史"},
|
||||||
|
"DELETE /sysParams/deleteSysParams": {group: "参数管理", description: "删除参数"},
|
||||||
|
"DELETE /sysParams/deleteSysParamsByIds": {group: "参数管理", description: "批量删除参数"},
|
||||||
|
"DELETE /sysVersion/deleteSysVersion": {group: "版本控制", description: "删除版本"},
|
||||||
|
"DELETE /sysVersion/deleteSysVersionByIds": {group: "版本控制", description: "批量删除版本"},
|
||||||
|
"DELETE /timedTask/deleteTimedTask": {group: "定时任务", description: "删除定时任务"},
|
||||||
|
"DELETE /user/deleteUser": {group: "系统用户", description: "删除用户"},
|
||||||
|
"GET /api/getApiGroups": {group: "api", description: "获取路由组"},
|
||||||
|
"GET /api/getApiRoles": {group: "api", description: "获取指定API关联角色列表"},
|
||||||
|
"GET /api/syncApi": {group: "api", description: "获取待同步API"},
|
||||||
|
"GET /attachmentCategory/getCategoryList": {group: "媒体库分类", description: "分类列表"},
|
||||||
|
"GET /authority/getDataScopeDepts": {group: "角色", description: "获取角色自定义部门集"},
|
||||||
|
"GET /authority/getUsersByAuthority": {group: "角色", description: "获取角色关联用户ID列表"},
|
||||||
|
"GET /customer/customer": {group: "客户", description: "获取单一客户"},
|
||||||
|
"GET /customer/customerList": {group: "客户", description: "获取客户列表"},
|
||||||
|
"GET /department/findDepartment": {group: "部门", description: "根据ID获取部门"},
|
||||||
|
"GET /department/getDepartmentUsers": {group: "部门", description: "获取部门成员ID列表"},
|
||||||
|
"GET /info/findInfo": {group: "公告", description: "根据ID获取公告"},
|
||||||
|
"GET /info/getInfoList": {group: "公告", description: "获取公告列表"},
|
||||||
|
"GET /logViewer/content": {group: "文件日志", description: "分块读取日志文件内容"},
|
||||||
|
"GET /logViewer/dates": {group: "文件日志", description: "获取存在日志的日期"},
|
||||||
|
"GET /logViewer/files": {group: "文件日志", description: "获取日期下的日志文件"},
|
||||||
|
"GET /menu/getMenuRoles": {group: "菜单", description: "获取菜单关联角色列表"},
|
||||||
|
"GET /position/findPosition": {group: "岗位", description: "根据ID获取岗位"},
|
||||||
|
"GET /position/getPositionUsers": {group: "岗位", description: "获取岗位成员ID列表"},
|
||||||
|
"GET /securityConfig/getSecurityConfig": {group: "安全配置", description: "获取安全配置"},
|
||||||
|
"GET /simpleUploader/checkFileMd5": {group: "断点续传(插件版)", description: "文件完整度验证"},
|
||||||
|
"GET /simpleUploader/mergeFileMd5": {group: "断点续传(插件版)", description: "上传完成合并文件"},
|
||||||
|
"GET /sysDictionary/exportSysDictionary": {group: "系统字典", description: "导出字典JSON"},
|
||||||
|
"GET /sysDictionary/findSysDictionary": {group: "系统字典", description: "根据ID获取字典(建议选择)"},
|
||||||
|
"GET /sysDictionary/getSysDictionaryList": {group: "系统字典", description: "获取字典列表"},
|
||||||
|
"GET /sysDictionary/getSysDictionaryListWithDetails": {group: "系统字典", description: "获取字典列表(含明细)"},
|
||||||
|
"GET /sysDictionaryDetail/findSysDictionaryDetail": {group: "系统字典详情", description: "根据ID获取字典内容"},
|
||||||
|
"GET /sysDictionaryDetail/getDictionaryDetailsByParent": {group: "系统字典详情", description: "根据父级ID获取字典详情"},
|
||||||
|
"GET /sysDictionaryDetail/getDictionaryPath": {group: "系统字典详情", description: "获取字典详情的完整路径"},
|
||||||
|
"GET /sysDictionaryDetail/getDictionaryTreeList": {group: "系统字典详情", description: "获取字典数列表"},
|
||||||
|
"GET /sysDictionaryDetail/getDictionaryTreeListByType": {group: "系统字典详情", description: "根据分类获取字典数列表"},
|
||||||
|
"GET /sysDictionaryDetail/getSysDictionaryDetailList": {group: "系统字典详情", description: "获取字典内容列表"},
|
||||||
|
"GET /sysError/findSysError": {group: "错误日志", description: "根据ID获取错误日志"},
|
||||||
|
"GET /sysError/getSysErrorList": {group: "错误日志", description: "获取错误日志列表"},
|
||||||
|
"GET /sysError/getSysErrorSolution": {group: "错误日志", description: "触发错误处理(异步)"},
|
||||||
|
"GET /sysExportTemplate/exportExcel": {group: "导出模板", description: "导出Excel"},
|
||||||
|
"GET /sysExportTemplate/exportTemplate": {group: "导出模板", description: "下载模板"},
|
||||||
|
"GET /sysExportTemplate/findSysExportTemplate": {group: "导出模板", description: "根据ID获取导出模板"},
|
||||||
|
"GET /sysExportTemplate/getSysExportTemplateList": {group: "导出模板", description: "获取导出模板列表"},
|
||||||
|
"GET /sysExportTemplate/previewSQL": {group: "导出模板", description: "预览SQL"},
|
||||||
|
"GET /sysLoginLog/findLoginLog": {group: "登录日志", description: "根据ID获取登录日志"},
|
||||||
|
"GET /sysLoginLog/getLoginLogList": {group: "登录日志", description: "获取登录日志列表"},
|
||||||
|
"GET /sysOperationRecord/findSysOperationRecord": {group: "操作记录", description: "根据ID获取操作记录"},
|
||||||
|
"GET /sysOperationRecord/getSysOperationRecordList": {group: "操作记录", description: "获取操作记录列表"},
|
||||||
|
"GET /sysParams/findSysParams": {group: "参数管理", description: "根据ID获取参数"},
|
||||||
|
"GET /sysParams/getSysParam": {group: "参数管理", description: "获取参数列表"},
|
||||||
|
"GET /sysParams/getSysParamsList": {group: "参数管理", description: "获取参数列表"},
|
||||||
|
"GET /sysVersion/downloadVersionJson": {group: "版本控制", description: "下载版本json"},
|
||||||
|
"GET /sysVersion/findSysVersion": {group: "版本控制", description: "获取单一版本"},
|
||||||
|
"GET /sysVersion/getSysVersionList": {group: "版本控制", description: "获取版本列表"},
|
||||||
|
"GET /timedTask/alertStream": {group: "定时任务", description: "订阅定时任务失败告警(SSE)"},
|
||||||
|
"GET /timedTask/getRegisteredMethods": {group: "定时任务", description: "获取已注册方法列表"},
|
||||||
|
"GET /timedTask/getTimedTaskList": {group: "定时任务", description: "获取定时任务列表"},
|
||||||
|
"GET /timedTask/getTimedTaskLogList": {group: "定时任务", description: "获取定时任务执行日志"},
|
||||||
|
"GET /user/getUserInfo": {group: "系统用户", description: "获取自身信息(必选)"},
|
||||||
|
"POST /api/createApi": {group: "api", description: "创建api"},
|
||||||
|
"POST /api/deleteApi": {group: "api", description: "删除Api"},
|
||||||
|
"POST /api/enterSyncApi": {group: "api", description: "确认同步API"},
|
||||||
|
"POST /api/getAllApis": {group: "api", description: "获取所有api"},
|
||||||
|
"POST /api/getApiById": {group: "api", description: "获取api详细信息"},
|
||||||
|
"POST /api/getApiList": {group: "api", description: "获取api列表"},
|
||||||
|
"POST /api/ignoreApi": {group: "api", description: "忽略API"},
|
||||||
|
"POST /api/setApiRoles": {group: "api", description: "全量覆盖API关联角色列表"},
|
||||||
|
"POST /api/updateApi": {group: "api", description: "更新Api"},
|
||||||
|
"POST /attachmentCategory/addCategory": {group: "媒体库分类", description: "添加/编辑分类"},
|
||||||
|
"POST /attachmentCategory/deleteCategory": {group: "媒体库分类", description: "删除分类"},
|
||||||
|
"POST /authority/copyAuthority": {group: "角色", description: "拷贝角色"},
|
||||||
|
"POST /authority/createAuthority": {group: "角色", description: "创建角色"},
|
||||||
|
"POST /authority/deleteAuthority": {group: "角色", description: "删除角色"},
|
||||||
|
"POST /authority/getAuthorityList": {group: "角色", description: "获取角色列表"},
|
||||||
|
"POST /authority/setDataScope": {group: "角色", description: "设置角色数据权限"},
|
||||||
|
"POST /authority/setRoleUsers": {group: "角色", description: "全量覆盖角色关联用户"},
|
||||||
|
"POST /authorityBtn/canRemoveAuthorityBtn": {group: "按钮权限", description: "删除按钮"},
|
||||||
|
"POST /authorityBtn/getAuthorityBtn": {group: "按钮权限", description: "获取已有按钮权限"},
|
||||||
|
"POST /authorityBtn/setAuthorityBtn": {group: "按钮权限", description: "设置按钮权限"},
|
||||||
|
"POST /autoCode/initAPI": {group: "代码生成器", description: "生成插件 API 初始化文件"},
|
||||||
|
"POST /autoCode/initDictionary": {group: "代码生成器", description: "生成插件字典初始化文件"},
|
||||||
|
"POST /autoCode/initMenu": {group: "代码生成器", description: "生成插件菜单初始化文件"},
|
||||||
|
"POST /casbin/getPolicyPathByAuthorityId": {group: "casbin", description: "获取权限列表"},
|
||||||
|
"POST /casbin/updateCasbin": {group: "casbin", description: "更改角色api权限"},
|
||||||
|
"POST /customer/customer": {group: "客户", description: "创建客户"},
|
||||||
|
"POST /dataAccessLog/getDataAccessLogList": {group: "数据权限审计", description: "获取数据权限审计日志"},
|
||||||
|
"POST /department/createDepartment": {group: "部门", description: "创建部门"},
|
||||||
|
"POST /department/getDepartmentList": {group: "部门", description: "获取部门树"},
|
||||||
|
"POST /department/setDepartmentUsers": {group: "部门", description: "设置部门成员(反向分配)"},
|
||||||
|
"POST /email/emailTest": {group: "email", description: "发送测试邮件"},
|
||||||
|
"POST /email/sendEmail": {group: "email", description: "发送邮件"},
|
||||||
|
"POST /fileUploadAndDownload/deleteFile": {group: "文件上传与下载", description: "删除文件"},
|
||||||
|
"POST /fileUploadAndDownload/editFileName": {group: "文件上传与下载", description: "文件名或者备注编辑"},
|
||||||
|
"POST /fileUploadAndDownload/getFileList": {group: "文件上传与下载", description: "获取上传文件列表"},
|
||||||
|
"POST /fileUploadAndDownload/importURL": {group: "文件上传与下载", description: "导入URL"},
|
||||||
|
"POST /fileUploadAndDownload/upload": {group: "文件上传与下载", description: "文件上传(建议选择)"},
|
||||||
|
"POST /info/createInfo": {group: "公告", description: "新建公告"},
|
||||||
|
"POST /jwt/jsonInBlacklist": {group: "jwt", description: "jwt加入黑名单(退出,必选)"},
|
||||||
|
"POST /mediaUpload/chunk": {group: "媒体上传", description: "上传分片"},
|
||||||
|
"POST /mediaUpload/complete": {group: "媒体上传", description: "完成大文件上传"},
|
||||||
|
"POST /mediaUpload/init": {group: "媒体上传", description: "初始化大文件上传"},
|
||||||
|
"POST /menu/addBaseMenu": {group: "菜单", description: "新增菜单"},
|
||||||
|
"POST /menu/addMenuAuthority": {group: "菜单", description: "增加menu和角色关联关系"},
|
||||||
|
"POST /menu/deleteBaseMenu": {group: "菜单", description: "删除菜单"},
|
||||||
|
"POST /menu/getBaseMenuById": {group: "菜单", description: "根据id获取菜单"},
|
||||||
|
"POST /menu/getBaseMenuTree": {group: "菜单", description: "获取用户动态路由"},
|
||||||
|
"POST /menu/getMenu": {group: "菜单", description: "获取菜单树(必选)"},
|
||||||
|
"POST /menu/getMenuAuthority": {group: "菜单", description: "获取指定角色menu"},
|
||||||
|
"POST /menu/getMenuList": {group: "菜单", description: "分页获取基础menu列表"},
|
||||||
|
"POST /menu/setMenuRoles": {group: "菜单", description: "全量覆盖菜单关联角色列表"},
|
||||||
|
"POST /menu/updateBaseMenu": {group: "菜单", description: "更新菜单"},
|
||||||
|
"POST /position/createPosition": {group: "岗位", description: "创建岗位"},
|
||||||
|
"POST /position/getPositionList": {group: "岗位", description: "获取岗位列表"},
|
||||||
|
"POST /position/setPositionUsers": {group: "岗位", description: "设置岗位成员(反向分配)"},
|
||||||
|
"POST /securityConfig/setSecurityConfig": {group: "安全配置", description: "设置安全配置"},
|
||||||
|
"POST /simpleUploader/upload": {group: "断点续传(插件版)", description: "插件版分片上传"},
|
||||||
|
"POST /sysApiToken/createApiToken": {group: "API Token", description: "签发API Token"},
|
||||||
|
"POST /sysApiToken/deleteApiToken": {group: "API Token", description: "作废API Token"},
|
||||||
|
"POST /sysApiToken/getApiTokenList": {group: "API Token", description: "获取API Token列表"},
|
||||||
|
"POST /sysDictionary/createSysDictionary": {group: "系统字典", description: "新增字典"},
|
||||||
|
"POST /sysDictionary/importSysDictionary": {group: "系统字典", description: "导入字典JSON"},
|
||||||
|
"POST /sysDictionaryDetail/createSysDictionaryDetail": {group: "系统字典详情", description: "新增字典内容"},
|
||||||
|
"POST /sysError/createSysError": {group: "错误日志", description: "新建错误日志"},
|
||||||
|
"POST /sysExportTemplate/createSysExportTemplate": {group: "导出模板", description: "新增导出模板"},
|
||||||
|
"POST /sysExportTemplate/importExcel": {group: "导出模板", description: "导入Excel"},
|
||||||
|
"POST /sysOperationRecord/createSysOperationRecord": {group: "操作记录", description: "新增操作记录"},
|
||||||
|
"POST /sysParams/createSysParams": {group: "参数管理", description: "新建参数"},
|
||||||
|
"POST /system/getServerInfo": {group: "系统服务", description: "获取服务器信息"},
|
||||||
|
"POST /system/getSystemConfig": {group: "系统服务", description: "获取配置文件内容"},
|
||||||
|
"POST /system/setSystemConfig": {group: "系统服务", description: "设置配置文件内容"},
|
||||||
|
"POST /sysVersion/exportVersion": {group: "版本控制", description: "创建版本"},
|
||||||
|
"POST /sysVersion/importVersion": {group: "版本控制", description: "同步版本"},
|
||||||
|
"POST /timedTask/createTimedTask": {group: "定时任务", description: "创建定时任务"},
|
||||||
|
"POST /timedTask/toggleTimedTask": {group: "定时任务", description: "启用/停用定时任务"},
|
||||||
|
"POST /timedTask/triggerTimedTask": {group: "定时任务", description: "手动触发定时任务"},
|
||||||
|
"POST /user/admin_register": {group: "系统用户", description: "用户注册"},
|
||||||
|
"POST /user/changePassword": {group: "系统用户", description: "修改密码(建议选择)"},
|
||||||
|
"POST /user/getUserList": {group: "系统用户", description: "获取用户列表"},
|
||||||
|
"POST /user/resetPassword": {group: "系统用户", description: "重置用户密码"},
|
||||||
|
"POST /user/setUserAuthorities": {group: "系统用户", description: "设置权限组"},
|
||||||
|
"POST /user/setUserAuthority": {group: "系统用户", description: "修改用户角色(必选)"},
|
||||||
|
"POST /user/setUserDepartments": {group: "系统用户", description: "设置用户归属部门"},
|
||||||
|
"POST /user/setUserPositions": {group: "系统用户", description: "设置用户岗位"},
|
||||||
|
"PUT /authority/updateAuthority": {group: "角色", description: "更新角色信息"},
|
||||||
|
"PUT /customer/customer": {group: "客户", description: "更新客户"},
|
||||||
|
"PUT /department/updateDepartment": {group: "部门", description: "更新部门"},
|
||||||
|
"PUT /info/updateInfo": {group: "公告", description: "更新公告"},
|
||||||
|
"PUT /position/updatePosition": {group: "岗位", description: "更新岗位"},
|
||||||
|
"PUT /sysDictionary/updateSysDictionary": {group: "系统字典", description: "更新字典"},
|
||||||
|
"PUT /sysDictionaryDetail/updateSysDictionaryDetail": {group: "系统字典详情", description: "更新字典内容"},
|
||||||
|
"PUT /sysError/updateSysError": {group: "错误日志", description: "更新错误日志"},
|
||||||
|
"PUT /sysExportTemplate/updateSysExportTemplate": {group: "导出模板", description: "更新导出模板"},
|
||||||
|
"PUT /sysParams/updateSysParams": {group: "参数管理", description: "更新参数"},
|
||||||
|
"PUT /timedTask/updateTimedTask": {group: "定时任务", description: "更新定时任务"},
|
||||||
|
"PUT /user/setSelfInfo": {group: "系统用户", description: "设置自身信息(必选)"},
|
||||||
|
"PUT /user/setSelfSetting": {group: "系统用户", description: "用户界面配置"},
|
||||||
|
"PUT /user/setUserInfo": {group: "系统用户", description: "设置用户信息"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func routeMetadata(method, path string) (string, string) {
|
||||||
|
if value, ok := apiMetadata[strings.ToUpper(method)+" "+path]; ok {
|
||||||
|
return value.group, value.description
|
||||||
|
}
|
||||||
|
return routeGroup(path), ""
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,11 @@ import (
|
||||||
"kra/pkg/adminauth"
|
"kra/pkg/adminauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UserDisabledError struct{ UserID uint }
|
||||||
|
|
||||||
|
func (e *UserDisabledError) Error() string { return biz.ErrUserDisabled.Error() }
|
||||||
|
func (e *UserDisabledError) Unwrap() error { return biz.ErrUserDisabled }
|
||||||
|
|
||||||
type LoginResult struct {
|
type LoginResult struct {
|
||||||
User map[string]any `json:"user"`
|
User map[string]any `json:"user"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
|
@ -34,7 +39,7 @@ func (s *SystemService) Login(ctx context.Context, username, password string) (*
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if u.Enable != 1 {
|
if u.Enable != 1 {
|
||||||
return nil, biz.ErrUserDisabled
|
return nil, &UserDisabledError{UserID: u.ID}
|
||||||
}
|
}
|
||||||
if security, securityErr := s.settings.CurrentSecurity(ctx); securityErr == nil && security.PwdExpireEnable && u.PasswordUpdatedAt != nil && time.Since(*u.PasswordUpdatedAt) > time.Duration(security.PwdExpireDays)*24*time.Hour {
|
if security, securityErr := s.settings.CurrentSecurity(ctx); securityErr == nil && security.PwdExpireEnable && u.PasswordUpdatedAt != nil && time.Since(*u.PasswordUpdatedAt) > time.Duration(security.PwdExpireDays)*24*time.Hour {
|
||||||
u.MustChangePassword = true
|
u.MustChangePassword = true
|
||||||
|
|
|
||||||
|
|
@ -29,22 +29,37 @@ type SetSystemConfigRequest struct {
|
||||||
Config struct {
|
Config struct {
|
||||||
Data *struct {
|
Data *struct {
|
||||||
Database *struct {
|
Database *struct {
|
||||||
Driver string `json:"driver"`
|
Driver string `json:"driver"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Port string `json:"port"`
|
Port string `json:"port"`
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Config string `json:"config"`
|
Config string `json:"config"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
Prefix string `json:"prefix"`
|
||||||
|
Engine string `json:"engine"`
|
||||||
|
LogMode string `json:"log_mode"`
|
||||||
|
MaxIdleConns int32 `json:"max_idle_conns"`
|
||||||
|
MaxOpenConns int32 `json:"max_open_conns"`
|
||||||
|
ConnMaxLifetime int32 `json:"conn_max_lifetime"`
|
||||||
|
Singular bool `json:"singular"`
|
||||||
} `json:"database"`
|
} `json:"database"`
|
||||||
Redis *struct {
|
Redis *struct {
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
ReadTimeout string `json:"read_timeout"`
|
ReadTimeout string `json:"read_timeout"`
|
||||||
WriteTimeout string `json:"write_timeout"`
|
WriteTimeout string `json:"write_timeout"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
DB int32 `json:"db"`
|
||||||
|
UseCluster bool `json:"use_cluster"`
|
||||||
|
ClusterAddrs []string `json:"cluster_addrs"`
|
||||||
} `json:"redis"`
|
} `json:"redis"`
|
||||||
|
DatabaseList []*conf.Data_Database `json:"database_list"`
|
||||||
|
RedisList []*conf.Data_Redis `json:"redis_list"`
|
||||||
|
Mongo *conf.Data_Mongo `json:"mongo"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
Admin struct {
|
Admin struct {
|
||||||
RouterPrefix string `json:"routerPrefix"`
|
RouterPrefix string `json:"routerPrefix"`
|
||||||
|
|
@ -53,6 +68,7 @@ type SetSystemConfigRequest struct {
|
||||||
UseMultipoint bool `json:"useMultipoint"`
|
UseMultipoint bool `json:"useMultipoint"`
|
||||||
UseStrictAuth bool `json:"useStrictAuth"`
|
UseStrictAuth bool `json:"useStrictAuth"`
|
||||||
DisableAutoMigrate bool `json:"disableAutoMigrate"`
|
DisableAutoMigrate bool `json:"disableAutoMigrate"`
|
||||||
|
UseMongo bool `json:"useMongo"`
|
||||||
} `json:"system"`
|
} `json:"system"`
|
||||||
JWT struct {
|
JWT struct {
|
||||||
SigningKey string `json:"signingKey"`
|
SigningKey string `json:"signingKey"`
|
||||||
|
|
@ -74,6 +90,9 @@ type SetSystemConfigRequest struct {
|
||||||
SessionTTL int32 `json:"sessionTtl"`
|
SessionTTL int32 `json:"sessionTtl"`
|
||||||
} `json:"media"`
|
} `json:"media"`
|
||||||
Storage *conf.AdminBackend_Storage `json:"storage"`
|
Storage *conf.AdminBackend_Storage `json:"storage"`
|
||||||
|
Zap *conf.AdminBackend_Zap `json:"zap"`
|
||||||
|
Cors *conf.AdminBackend_CORS `json:"cors"`
|
||||||
|
App *conf.AdminBackend_App `json:"app"`
|
||||||
} `json:"admin"`
|
} `json:"admin"`
|
||||||
Email *struct {
|
Email *struct {
|
||||||
To string `json:"to"`
|
To string `json:"to"`
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ func (s *SystemService) SystemConfig() map[string]any {
|
||||||
}
|
}
|
||||||
admin["routerPrefix"] = config.RouterPrefix
|
admin["routerPrefix"] = config.RouterPrefix
|
||||||
if config.System != nil {
|
if config.System != nil {
|
||||||
admin["system"] = map[string]any{"useRedis": config.System.UseRedis, "useMultipoint": config.System.UseMultipoint, "useStrictAuth": config.System.UseStrictAuth, "disableAutoMigrate": config.System.DisableAutoMigrate}
|
admin["system"] = map[string]any{"useRedis": config.System.UseRedis, "useMultipoint": config.System.UseMultipoint, "useStrictAuth": config.System.UseStrictAuth, "disableAutoMigrate": config.System.DisableAutoMigrate, "useMongo": config.System.UseMongo}
|
||||||
}
|
}
|
||||||
if config.Jwt != nil {
|
if config.Jwt != nil {
|
||||||
admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": config.Jwt.ExpiresTime.AsDuration().String(), "bufferTime": config.Jwt.BufferTime.AsDuration().String(), "issuer": config.Jwt.Issuer}
|
admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": config.Jwt.ExpiresTime.AsDuration().String(), "bufferTime": config.Jwt.BufferTime.AsDuration().String(), "issuer": config.Jwt.Issuer}
|
||||||
|
|
@ -70,16 +70,36 @@ func (s *SystemService) SystemConfig() map[string]any {
|
||||||
maskStorageSecrets(storage)
|
maskStorageSecrets(storage)
|
||||||
admin["storage"] = storage
|
admin["storage"] = storage
|
||||||
}
|
}
|
||||||
|
if config.Zap != nil {
|
||||||
|
admin["zap"] = config.Zap
|
||||||
|
}
|
||||||
|
if config.Cors != nil {
|
||||||
|
admin["cors"] = config.Cors
|
||||||
|
}
|
||||||
|
if config.App != nil {
|
||||||
|
admin["app"] = config.App
|
||||||
|
}
|
||||||
data := s.runtime.Data()
|
data := s.runtime.Data()
|
||||||
if data != nil {
|
if data != nil {
|
||||||
if data.Database != nil {
|
if data.Database != nil {
|
||||||
data.Database.Password = "******"
|
data.Database.Password = "******"
|
||||||
}
|
}
|
||||||
|
if data.Redis != nil {
|
||||||
|
data.Redis.Password = "******"
|
||||||
|
}
|
||||||
|
if data.Mongo != nil {
|
||||||
|
data.Mongo.Password = "******"
|
||||||
|
}
|
||||||
for _, database := range data.DatabaseList {
|
for _, database := range data.DatabaseList {
|
||||||
if database != nil {
|
if database != nil {
|
||||||
database.Password = "******"
|
database.Password = "******"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, redis := range data.RedisList {
|
||||||
|
if redis != nil {
|
||||||
|
redis.Password = "******"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dataMap := map[string]any{}
|
dataMap := map[string]any{}
|
||||||
if raw, err := (protojson.MarshalOptions{UseProtoNames: true}).Marshal(data); err == nil {
|
if raw, err := (protojson.MarshalOptions{UseProtoNames: true}).Marshal(data); err == nil {
|
||||||
|
|
@ -103,6 +123,16 @@ func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystem
|
||||||
next.System.UseMultipoint = req.Config.Admin.System.UseMultipoint
|
next.System.UseMultipoint = req.Config.Admin.System.UseMultipoint
|
||||||
next.System.UseStrictAuth = req.Config.Admin.System.UseStrictAuth
|
next.System.UseStrictAuth = req.Config.Admin.System.UseStrictAuth
|
||||||
next.System.DisableAutoMigrate = req.Config.Admin.System.DisableAutoMigrate
|
next.System.DisableAutoMigrate = req.Config.Admin.System.DisableAutoMigrate
|
||||||
|
next.System.UseMongo = req.Config.Admin.System.UseMongo
|
||||||
|
if req.Config.Admin.Zap != nil {
|
||||||
|
next.Zap = req.Config.Admin.Zap
|
||||||
|
}
|
||||||
|
if req.Config.Admin.Cors != nil {
|
||||||
|
next.Cors = req.Config.Admin.Cors
|
||||||
|
}
|
||||||
|
if req.Config.Admin.App != nil {
|
||||||
|
next.App = req.Config.Admin.App
|
||||||
|
}
|
||||||
if next.Jwt != nil {
|
if next.Jwt != nil {
|
||||||
if req.Config.Admin.JWT.SigningKey != "" && req.Config.Admin.JWT.SigningKey != "******" {
|
if req.Config.Admin.JWT.SigningKey != "" && req.Config.Admin.JWT.SigningKey != "******" {
|
||||||
next.Jwt.SigningKey = req.Config.Admin.JWT.SigningKey
|
next.Jwt.SigningKey = req.Config.Admin.JWT.SigningKey
|
||||||
|
|
@ -163,10 +193,14 @@ func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystem
|
||||||
if data.Database != nil && (password == "" || password == "******") {
|
if data.Database != nil && (password == "" || password == "******") {
|
||||||
password = data.Database.Password
|
password = data.Database.Password
|
||||||
}
|
}
|
||||||
data.Database = &conf.Data_Database{Driver: value.Driver, Source: value.Source, Host: value.Host, Port: value.Port, User: value.User, Password: password, Name: value.Name, Config: value.Config, Path: value.Path}
|
data.Database = &conf.Data_Database{Driver: value.Driver, Source: value.Source, Host: value.Host, Port: value.Port, User: value.User, Password: password, Name: value.Name, Config: value.Config, Path: value.Path, Prefix: value.Prefix, Engine: value.Engine, LogMode: value.LogMode, MaxIdleConns: value.MaxIdleConns, MaxOpenConns: value.MaxOpenConns, ConnMaxLifetime: value.ConnMaxLifetime, Singular: value.Singular}
|
||||||
}
|
}
|
||||||
if value := req.Config.Data.Redis; value != nil {
|
if value := req.Config.Data.Redis; value != nil {
|
||||||
redis := &conf.Data_Redis{Network: value.Network, Addr: value.Addr}
|
password := value.Password
|
||||||
|
if data.Redis != nil && (password == "" || password == "******") {
|
||||||
|
password = data.Redis.Password
|
||||||
|
}
|
||||||
|
redis := &conf.Data_Redis{Network: value.Network, Addr: value.Addr, Name: value.Name, Password: password, Db: value.DB, UseCluster: value.UseCluster, ClusterAddrs: value.ClusterAddrs}
|
||||||
if duration, parseErr := time.ParseDuration(value.ReadTimeout); parseErr == nil && duration >= 0 {
|
if duration, parseErr := time.ParseDuration(value.ReadTimeout); parseErr == nil && duration >= 0 {
|
||||||
redis.ReadTimeout = durationpb.New(duration)
|
redis.ReadTimeout = durationpb.New(duration)
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +209,28 @@ func (s *SystemService) SaveSystemConfig(ctx context.Context, req *dto.SetSystem
|
||||||
}
|
}
|
||||||
data.Redis = redis
|
data.Redis = redis
|
||||||
}
|
}
|
||||||
|
if req.Config.Data.DatabaseList != nil {
|
||||||
|
for i, item := range req.Config.Data.DatabaseList {
|
||||||
|
if item != nil && (item.Password == "" || item.Password == "******") && i < len(data.DatabaseList) && data.DatabaseList[i] != nil {
|
||||||
|
item.Password = data.DatabaseList[i].Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.DatabaseList = req.Config.Data.DatabaseList
|
||||||
|
}
|
||||||
|
if req.Config.Data.RedisList != nil {
|
||||||
|
for i, item := range req.Config.Data.RedisList {
|
||||||
|
if item != nil && (item.Password == "" || item.Password == "******") && i < len(data.RedisList) && data.RedisList[i] != nil {
|
||||||
|
item.Password = data.RedisList[i].Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.RedisList = req.Config.Data.RedisList
|
||||||
|
}
|
||||||
|
if req.Config.Data.Mongo != nil {
|
||||||
|
if data.Mongo != nil && (req.Config.Data.Mongo.Password == "" || req.Config.Data.Mongo.Password == "******") {
|
||||||
|
req.Config.Data.Mongo.Password = data.Mongo.Password
|
||||||
|
}
|
||||||
|
data.Mongo = req.Config.Data.Mongo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(data)
|
dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ type DatabaseInit struct {
|
||||||
Port string `json:"port"`
|
Port string `json:"port"`
|
||||||
UserName string `json:"userName"`
|
UserName string `json:"userName"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
DBName string `json:"dbName"`
|
DBName string `json:"dbName" binding:"required"`
|
||||||
DBPath string `json:"dbPath"`
|
DBPath string `json:"dbPath"`
|
||||||
Template string `json:"template"`
|
Template string `json:"template"`
|
||||||
AdminPassword string `json:"adminPassword"`
|
AdminPassword string `json:"adminPassword" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit, apis []*biz.API) error {
|
func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit, apis []*biz.API) error {
|
||||||
|
|
@ -34,7 +34,15 @@ func (s *SystemService) Initialize(ctx context.Context, input *DatabaseInit, api
|
||||||
func (s *SystemService) InitializeRoutes(ctx context.Context, input *DatabaseInit, routes []dto.Route) error {
|
func (s *SystemService) InitializeRoutes(ctx context.Context, input *DatabaseInit, routes []dto.Route) error {
|
||||||
apis := make([]*biz.API, 0, len(routes))
|
apis := make([]*biz.API, 0, len(routes))
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
apis = append(apis, &biz.API{Path: route.Path, Method: route.Method, APIGroup: routeGroup(route.Path)})
|
path := route.Path
|
||||||
|
if config := s.runtime.Admin(); config != nil && config.RouterPrefix != "" {
|
||||||
|
path = strings.TrimPrefix(path, strings.TrimSuffix(config.RouterPrefix, "/"))
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
group, description := routeMetadata(route.Method, path)
|
||||||
|
apis = append(apis, &biz.API{Path: path, Method: route.Method, APIGroup: group, Description: description})
|
||||||
}
|
}
|
||||||
return s.Initialize(ctx, input, apis)
|
return s.Initialize(ctx, input, apis)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,14 @@ import (
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTokenExpired = errors.New("token expired")
|
||||||
|
ErrTokenMalformed = errors.New("token malformed")
|
||||||
|
ErrTokenSignatureInvalid = errors.New("token signature invalid")
|
||||||
|
ErrTokenNotValidYet = errors.New("token not valid yet")
|
||||||
|
ErrTokenInvalid = errors.New("token invalid")
|
||||||
|
)
|
||||||
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
UUID string
|
UUID string
|
||||||
ID uint
|
ID uint
|
||||||
|
|
@ -35,8 +43,22 @@ func Parse(tokenString, secret string) (*Claims, error) {
|
||||||
}
|
}
|
||||||
return []byte(secret), nil
|
return []byte(secret), nil
|
||||||
})
|
})
|
||||||
if err != nil || !token.Valid {
|
if err != nil {
|
||||||
return nil, errors.New("token is invalid")
|
switch {
|
||||||
|
case errors.Is(err, jwt.ErrTokenExpired):
|
||||||
|
return nil, ErrTokenExpired
|
||||||
|
case errors.Is(err, jwt.ErrTokenMalformed):
|
||||||
|
return nil, ErrTokenMalformed
|
||||||
|
case errors.Is(err, jwt.ErrTokenSignatureInvalid):
|
||||||
|
return nil, ErrTokenSignatureInvalid
|
||||||
|
case errors.Is(err, jwt.ErrTokenNotValidYet):
|
||||||
|
return nil, ErrTokenNotValidYet
|
||||||
|
default:
|
||||||
|
return nil, ErrTokenInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !token.Valid {
|
||||||
|
return nil, ErrTokenInvalid
|
||||||
}
|
}
|
||||||
claims, ok := token.Claims.(*Claims)
|
claims, ok := token.Claims.(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,38 @@ import (
|
||||||
// file handle open for the active day and rotates on the first write after
|
// file handle open for the active day and rotates on the first write after
|
||||||
// midnight, matching the directory layout consumed by the log viewer.
|
// midnight, matching the directory layout consumed by the log viewer.
|
||||||
type DailyWriter struct {
|
type DailyWriter struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
root string
|
root string
|
||||||
name string
|
name string
|
||||||
date string
|
date string
|
||||||
file *os.File
|
file *os.File
|
||||||
|
retentionDay int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDailyWriter(root, name string) *DailyWriter {
|
func NewDailyWriter(root, name string, retentionDay int) *DailyWriter {
|
||||||
return &DailyWriter{root: root, name: name}
|
w := &DailyWriter{root: root, name: name, retentionDay: retentionDay}
|
||||||
|
w.removeExpired()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *DailyWriter) removeExpired() {
|
||||||
|
if w.retentionDay <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entries, err := os.ReadDir(w.root)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cutoff := time.Now().AddDate(0, 0, -w.retentionDay)
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
date, err := time.Parse("2006-01-02", entry.Name())
|
||||||
|
if err == nil && date.Before(cutoff) {
|
||||||
|
_ = os.RemoveAll(filepath.Join(w.root, entry.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *DailyWriter) Write(value []byte) (int, error) {
|
func (w *DailyWriter) Write(value []byte) (int, error) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package logging
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-kratos/kratos/contrib/otel/v3/tracing"
|
"github.com/go-kratos/kratos/contrib/otel/v3/tracing"
|
||||||
kratoslog "github.com/go-kratos/kratos/v3/log"
|
kratoslog "github.com/go-kratos/kratos/v3/log"
|
||||||
|
|
@ -11,20 +13,107 @@ import (
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Level, Format, EncodeLevel, Prefix, StacktraceKey string
|
||||||
|
LogInConsole, ShowLine bool
|
||||||
|
RetentionDay int
|
||||||
|
FileOnlyModules []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type moduleFilterCore struct {
|
||||||
|
zapcore.Core
|
||||||
|
fileOnly map[string]struct{}
|
||||||
|
mod string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *moduleFilterCore) With(fields []zapcore.Field) zapcore.Core {
|
||||||
|
mod := c.mod
|
||||||
|
if value := moduleField(fields); value != "" {
|
||||||
|
mod = value
|
||||||
|
}
|
||||||
|
return &moduleFilterCore{Core: c.Core.With(fields), fileOnly: c.fileOnly, mod: mod}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *moduleFilterCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||||
|
if c.Enabled(entry.Level) {
|
||||||
|
return checked.AddCore(entry, c)
|
||||||
|
}
|
||||||
|
return checked
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *moduleFilterCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||||
|
mod := c.mod
|
||||||
|
if value := moduleField(fields); value != "" {
|
||||||
|
mod = value
|
||||||
|
}
|
||||||
|
if _, excluded := c.fileOnly[mod]; excluded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.Core.Write(entry, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moduleField(fields []zapcore.Field) string {
|
||||||
|
for _, field := range fields {
|
||||||
|
if field.Key == "mod" {
|
||||||
|
return field.String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// NewZapLogger adapts a Zap core to the slog logger used by Kratos v3.
|
// NewZapLogger adapts a Zap core to the slog logger used by Kratos v3.
|
||||||
// The file layout remains compatible with the administration log viewer.
|
// The file layout remains compatible with the administration log viewer.
|
||||||
func NewZapLogger(root, filename string, attrs ...any) (*slog.Logger, func()) {
|
func NewZapLogger(root, filename string, options Options, attrs ...any) (*slog.Logger, func()) {
|
||||||
file := NewDailyWriter(root, filename)
|
file := NewDailyWriter(root, filename, options.RetentionDay)
|
||||||
encoder := zap.NewProductionEncoderConfig()
|
encoder := zap.NewProductionEncoderConfig()
|
||||||
encoder.EncodeTime = zapcore.ISO8601TimeEncoder
|
encoder.EncodeTime = zapcore.RFC3339NanoTimeEncoder
|
||||||
encoder.EncodeLevel = zapcore.LowercaseLevelEncoder
|
if options.StacktraceKey != "" {
|
||||||
core := zapcore.NewCore(
|
encoder.StacktraceKey = options.StacktraceKey
|
||||||
zapcore.NewJSONEncoder(encoder),
|
}
|
||||||
zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(file)),
|
switch options.EncodeLevel {
|
||||||
zap.NewAtomicLevelAt(zap.InfoLevel),
|
case "CapitalLevelEncoder":
|
||||||
|
encoder.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||||
|
case "CapitalColorLevelEncoder":
|
||||||
|
encoder.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||||
|
case "LowercaseColorLevelEncoder":
|
||||||
|
encoder.EncodeLevel = zapcore.LowercaseColorLevelEncoder
|
||||||
|
default:
|
||||||
|
encoder.EncodeLevel = zapcore.LowercaseLevelEncoder
|
||||||
|
}
|
||||||
|
var outputEncoder zapcore.Encoder = zapcore.NewJSONEncoder(encoder)
|
||||||
|
if options.Format != "" && options.Format != "json" {
|
||||||
|
if options.Prefix != "" {
|
||||||
|
encoder.EncodeTime = func(value time.Time, output zapcore.PrimitiveArrayEncoder) {
|
||||||
|
output.AppendString(options.Prefix + value.Format("2006-01-02 15:04:05.000"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputEncoder = zapcore.NewConsoleEncoder(encoder)
|
||||||
|
}
|
||||||
|
level := zap.InfoLevel
|
||||||
|
if parsed := level.Set(strings.ToLower(options.Level)); parsed != nil {
|
||||||
|
level = zap.DebugLevel
|
||||||
|
}
|
||||||
|
levelEnabler := zap.NewAtomicLevelAt(level)
|
||||||
|
fileCore := zapcore.NewCore(
|
||||||
|
outputEncoder.Clone(),
|
||||||
|
zapcore.AddSync(file),
|
||||||
|
levelEnabler,
|
||||||
)
|
)
|
||||||
|
core := zapcore.Core(fileCore)
|
||||||
|
if options.LogInConsole {
|
||||||
|
fileOnly := make(map[string]struct{}, len(options.FileOnlyModules))
|
||||||
|
for _, module := range options.FileOnlyModules {
|
||||||
|
fileOnly[module] = struct{}{}
|
||||||
|
}
|
||||||
|
consoleCore := zapcore.NewCore(outputEncoder.Clone(), zapcore.AddSync(os.Stdout), levelEnabler)
|
||||||
|
core = zapcore.NewTee(fileCore, &moduleFilterCore{Core: consoleCore, fileOnly: fileOnly})
|
||||||
|
}
|
||||||
zapLogger := zap.New(core)
|
zapLogger := zap.New(core)
|
||||||
handler := zapslog.NewHandler(zapLogger.Core(), zapslog.WithCaller(true), zapslog.AddStacktraceAt(slog.LevelError))
|
handlerOptions := []zapslog.HandlerOption{zapslog.AddStacktraceAt(slog.LevelError)}
|
||||||
|
if options.ShowLine {
|
||||||
|
handlerOptions = append(handlerOptions, zapslog.WithCaller(true))
|
||||||
|
}
|
||||||
|
handler := zapslog.NewHandler(zapLogger.Core(), handlerOptions...)
|
||||||
logger := kratoslog.NewLogger(handler, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...)
|
logger := kratoslog.NewLogger(handler, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...)
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
_ = zapLogger.Sync()
|
_ = zapLogger.Sync()
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,13 @@
|
||||||
<el-form-item label="数据库名"><el-input v-model.trim="config.data.database.name" /></el-form-item>
|
<el-form-item label="数据库名"><el-input v-model.trim="config.data.database.name" /></el-form-item>
|
||||||
<el-form-item label="SQLite 目录"><el-input v-model.trim="config.data.database.path" /></el-form-item>
|
<el-form-item label="SQLite 目录"><el-input v-model.trim="config.data.database.path" /></el-form-item>
|
||||||
<el-form-item label="连接参数"><el-input v-model.trim="config.data.database.config" /></el-form-item>
|
<el-form-item label="连接参数"><el-input v-model.trim="config.data.database.config" /></el-form-item>
|
||||||
|
<el-form-item label="表前缀"><el-input v-model.trim="config.data.database.prefix" /></el-form-item>
|
||||||
|
<el-form-item label="存储引擎"><el-input v-model.trim="config.data.database.engine" placeholder="InnoDB" /></el-form-item>
|
||||||
|
<el-form-item label="GORM 日志级别"><el-select v-model="config.data.database.log_mode" class="!w-full"><el-option label="Silent" value="silent" /><el-option label="Error" value="error" /><el-option label="Warn" value="warn" /><el-option label="Info" value="info" /></el-select></el-form-item>
|
||||||
|
<el-form-item label="最大空闲连接"><el-input-number v-model="config.data.database.max_idle_conns" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="最大打开连接"><el-input-number v-model="config.data.database.max_open_conns" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="连接最长复用(秒)"><el-input-number v-model="config.data.database.conn_max_lifetime" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="表名选项"><el-switch v-model="config.data.database.singular" active-text="使用单数表名" /></el-form-item>
|
||||||
<p class="md:col-span-2 text-sm text-gray-500">保存后点击“重载服务”,数据库连接会按新配置重建。</p>
|
<p class="md:col-span-2 text-sm text-gray-500">保存后点击“重载服务”,数据库连接会按新配置重建。</p>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
@ -85,11 +92,59 @@
|
||||||
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
|
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
|
||||||
<el-form-item label="网络"><el-input v-model.trim="config.data.redis.network" placeholder="tcp" /></el-form-item>
|
<el-form-item label="网络"><el-input v-model.trim="config.data.redis.network" placeholder="tcp" /></el-form-item>
|
||||||
<el-form-item label="地址"><el-input v-model.trim="config.data.redis.addr" placeholder="127.0.0.1:6379;留空关闭 Redis" /></el-form-item>
|
<el-form-item label="地址"><el-input v-model.trim="config.data.redis.addr" placeholder="127.0.0.1:6379;留空关闭 Redis" /></el-form-item>
|
||||||
|
<el-form-item label="实例名"><el-input v-model.trim="config.data.redis.name" /></el-form-item>
|
||||||
|
<el-form-item label="密码"><el-input v-model="config.data.redis.password" show-password /></el-form-item>
|
||||||
|
<el-form-item label="数据库"><el-input-number v-model="config.data.redis.db" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="集群模式"><el-switch v-model="config.data.redis.use_cluster" /></el-form-item>
|
||||||
|
<el-form-item v-if="config.data.redis.use_cluster" label="集群节点(每行一个)" class="md:col-span-2"><el-input v-model="redisClusterText" type="textarea" :rows="4" /></el-form-item>
|
||||||
<el-form-item label="读取超时"><el-input v-model.trim="config.data.redis.read_timeout" placeholder="例如 200ms" /></el-form-item>
|
<el-form-item label="读取超时"><el-input v-model.trim="config.data.redis.read_timeout" placeholder="例如 200ms" /></el-form-item>
|
||||||
<el-form-item label="写入超时"><el-input v-model.trim="config.data.redis.write_timeout" placeholder="例如 200ms" /></el-form-item>
|
<el-form-item label="写入超时"><el-input v-model.trim="config.data.redis.write_timeout" placeholder="例如 200ms" /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="MongoDB / 多数据库" name="advancedDatabase">
|
||||||
|
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
|
||||||
|
<el-form-item label="MongoDB 开关"><el-switch v-model="config.admin.system.useMongo" active-text="启用 MongoDB" /></el-form-item>
|
||||||
|
<el-form-item label="数据库名"><el-input v-model.trim="config.data.mongo.database" /></el-form-item>
|
||||||
|
<el-form-item label="默认集合"><el-input v-model.trim="config.data.mongo.coll" /></el-form-item>
|
||||||
|
<el-form-item label="连接选项"><el-input v-model.trim="config.data.mongo.options" /></el-form-item>
|
||||||
|
<el-form-item label="用户名"><el-input v-model.trim="config.data.mongo.username" /></el-form-item>
|
||||||
|
<el-form-item label="密码"><el-input v-model="config.data.mongo.password" show-password /></el-form-item>
|
||||||
|
<el-form-item label="认证库"><el-input v-model.trim="config.data.mongo.auth_source" /></el-form-item>
|
||||||
|
<el-form-item label="最小连接池"><el-input-number v-model="config.data.mongo.min_pool_size" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="最大连接池"><el-input-number v-model="config.data.mongo.max_pool_size" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="连接超时(毫秒)"><el-input-number v-model="config.data.mongo.connect_timeout_ms" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="Socket 超时(毫秒)"><el-input-number v-model="config.data.mongo.socket_timeout_ms" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="MongoDB 节点 JSON" class="md:col-span-2"><el-input v-model="mongoHostsText" type="textarea" :rows="4" placeholder='[{"host":"127.0.0.1","port":"27017"}]' /></el-form-item>
|
||||||
|
<el-form-item label="多数据库配置 JSON" class="md:col-span-2"><el-input v-model="databaseListText" type="textarea" :rows="6" /></el-form-item>
|
||||||
|
<el-form-item label="多 Redis 配置 JSON" class="md:col-span-2"><el-input v-model="redisListText" type="textarea" :rows="6" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="日志" name="logging">
|
||||||
|
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
|
||||||
|
<el-form-item label="日志级别"><el-select v-model="config.admin.zap.level" class="!w-full"><el-option label="Debug" value="debug" /><el-option label="Info" value="info" /><el-option label="Warn" value="warn" /><el-option label="Error" value="error" /></el-select></el-form-item>
|
||||||
|
<el-form-item label="格式"><el-select v-model="config.admin.zap.format" class="!w-full"><el-option label="JSON" value="json" /><el-option label="Console" value="console" /></el-select></el-form-item>
|
||||||
|
<el-form-item label="日志目录"><el-input v-model.trim="config.admin.zap.director" /></el-form-item>
|
||||||
|
<el-form-item label="日志前缀"><el-input v-model="config.admin.zap.prefix" /></el-form-item>
|
||||||
|
<el-form-item label="级别编码器"><el-select v-model="config.admin.zap.encode_level" class="!w-full"><el-option label="lowercase" value="LowercaseLevelEncoder" /><el-option label="lowercase color" value="LowercaseColorLevelEncoder" /><el-option label="capital" value="CapitalLevelEncoder" /><el-option label="capital color" value="CapitalColorLevelEncoder" /></el-select></el-form-item>
|
||||||
|
<el-form-item label="堆栈字段"><el-input v-model.trim="config.admin.zap.stacktrace_key" /></el-form-item>
|
||||||
|
<el-form-item label="保留天数"><el-input-number v-model="config.admin.zap.retention_day" :min="0" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="访问日志最大字节"><el-input-number v-model="config.admin.zap.access_log_max_bytes" :min="1024" class="!w-full" /></el-form-item>
|
||||||
|
<el-form-item label="输出选项" class="md:col-span-2"><div class="flex flex-wrap gap-5"><el-switch v-model="config.admin.zap.log_in_console" active-text="控制台" /><el-switch v-model="config.admin.zap.show_line" active-text="调用行" /><el-switch v-model="config.admin.zap.access_req_body" active-text="请求体" /><el-switch v-model="config.admin.zap.access_resp_data" active-text="响应体" /><el-switch v-model="config.admin.zap.access_req_headers" active-text="请求头" /></div></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="应用 / CORS" name="application">
|
||||||
|
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
|
||||||
|
<el-form-item label="节点标识"><el-input v-model.trim="config.admin.app.node" /></el-form-item>
|
||||||
|
<el-form-item label="应用 ID"><el-input v-model.trim="config.admin.app.app_id" /></el-form-item>
|
||||||
|
<el-form-item label="运行环境"><el-input v-model.trim="config.admin.app.env" /></el-form-item>
|
||||||
|
<el-form-item label="CORS 模式"><el-select v-model="config.admin.cors.mode" class="!w-full"><el-option label="白名单" value="whitelist" /><el-option label="全部放行" value="allow-all" /></el-select></el-form-item>
|
||||||
|
<el-form-item label="CORS 白名单 JSON" class="md:col-span-2"><el-input v-model="corsRulesText" type="textarea" :rows="7" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="对象存储" name="storage">
|
<el-tab-pane label="对象存储" name="storage">
|
||||||
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
|
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
|
||||||
<el-form-item label="存储类型" class="md:col-span-2">
|
<el-form-item label="存储类型" class="md:col-span-2">
|
||||||
|
|
@ -172,7 +227,9 @@
|
||||||
jwt: { signingKey: '******', expiresTime: '168h', bufferTime: '24h', issuer: 'kra' },
|
jwt: { signingKey: '******', expiresTime: '168h', bufferTime: '24h', issuer: 'kra' },
|
||||||
captcha: { keyLong: 6, imgWidth: 240, imgHeight: 80, storeExpiration: '3m' },
|
captcha: { keyLong: 6, imgWidth: 240, imgHeight: 80, storeExpiration: '3m' },
|
||||||
local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' }, media: { sessionTtl: 24 },
|
local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' }, media: { sessionTtl: 24 },
|
||||||
system: { useRedis: false, useMultipoint: false, useStrictAuth: false, disableAutoMigrate: false },
|
zap: { level: 'info', prefix: '[kra] ', format: 'json', director: 'logs', encode_level: 'LowercaseLevelEncoder', stacktrace_key: 'stacktrace', show_line: true, log_in_console: true, retention_day: 7, access_req_body: true, access_resp_data: true, access_req_headers: false, access_log_max_bytes: 32768, file_only_modules: [] },
|
||||||
|
cors: { mode: 'whitelist', whitelist: [] }, app: { node: '', app_id: 'kra', env: 'development' },
|
||||||
|
system: { useRedis: false, useMultipoint: false, useStrictAuth: false, disableAutoMigrate: false, useMongo: false },
|
||||||
storage: { type: 'local', qiniu: {}, aliyun_oss: {}, huawei_obs: {}, tencent_cos: {}, aws_s3: {}, cloudflare_r2: {}, minio: {} }
|
storage: { type: 'local', qiniu: {}, aliyun_oss: {}, huawei_obs: {}, tencent_cos: {}, aws_s3: {}, cloudflare_r2: {}, minio: {} }
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
|
|
@ -180,8 +237,9 @@
|
||||||
'is-ssl': true, 'is-loginauth': false
|
'is-ssl': true, 'is-loginauth': false
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
database: { driver: 'mysql', source: '', host: '127.0.0.1', port: '3306', user: '', password: '******', name: '', config: '', path: '' },
|
database: { driver: 'mysql', source: '', host: '127.0.0.1', port: '3306', user: '', password: '******', name: '', config: '', path: '', prefix: '', engine: 'InnoDB', log_mode: 'info', max_idle_conns: 10, max_open_conns: 100, conn_max_lifetime: 3600, singular: false },
|
||||||
redis: { network: 'tcp', addr: '', read_timeout: '200ms', write_timeout: '200ms' }
|
redis: { network: 'tcp', addr: '', name: 'default', password: '******', db: 0, use_cluster: false, cluster_addrs: [], read_timeout: '200ms', write_timeout: '200ms' },
|
||||||
|
database_list: [], redis_list: [], mongo: { coll: '', options: '', database: '', username: '', password: '******', auth_source: '', min_pool_size: 0, max_pool_size: 0, connect_timeout_ms: 0, socket_timeout_ms: 0, is_zap: false, hosts: [] }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const storageTypes = [
|
const storageTypes = [
|
||||||
|
|
@ -192,6 +250,11 @@
|
||||||
]
|
]
|
||||||
const storageKeys = { 'aliyun-oss': 'aliyun_oss', 'huawei-obs': 'huawei_obs', 'tencent-cos': 'tencent_cos', 'aws-s3': 'aws_s3', 'cloudflare-r2': 'cloudflare_r2', minio: 'minio' }
|
const storageKeys = { 'aliyun-oss': 'aliyun_oss', 'huawei-obs': 'huawei_obs', 'tencent-cos': 'tencent_cos', 'aws-s3': 'aws_s3', 'cloudflare-r2': 'cloudflare_r2', minio: 'minio' }
|
||||||
const currentObjectStorage = computed(() => config.value.admin.storage[storageKeys[config.value.admin.storage.type]])
|
const currentObjectStorage = computed(() => config.value.admin.storage[storageKeys[config.value.admin.storage.type]])
|
||||||
|
const redisClusterText = ref('')
|
||||||
|
const mongoHostsText = ref('[]')
|
||||||
|
const databaseListText = ref('[]')
|
||||||
|
const redisListText = ref('[]')
|
||||||
|
const corsRulesText = ref('[]')
|
||||||
|
|
||||||
const initForm = async () => {
|
const initForm = async () => {
|
||||||
const res = await getSystemConfig()
|
const res = await getSystemConfig()
|
||||||
|
|
@ -203,6 +266,9 @@
|
||||||
local: { ...config.value.admin.local, ...res.data.config.admin?.local }
|
local: { ...config.value.admin.local, ...res.data.config.admin?.local }
|
||||||
, media: { ...config.value.admin.media, ...res.data.config.admin?.media }
|
, media: { ...config.value.admin.media, ...res.data.config.admin?.media }
|
||||||
, system: { ...config.value.admin.system, ...res.data.config.admin?.system }
|
, system: { ...config.value.admin.system, ...res.data.config.admin?.system }
|
||||||
|
, zap: { ...config.value.admin.zap, ...res.data.config.admin?.zap }
|
||||||
|
, cors: { ...config.value.admin.cors, ...res.data.config.admin?.cors }
|
||||||
|
, app: { ...config.value.admin.app, ...res.data.config.admin?.app }
|
||||||
, storage: { ...config.value.admin.storage, ...res.data.config.admin?.storage,
|
, storage: { ...config.value.admin.storage, ...res.data.config.admin?.storage,
|
||||||
qiniu: { ...config.value.admin.storage.qiniu, ...res.data.config.admin?.storage?.qiniu },
|
qiniu: { ...config.value.admin.storage.qiniu, ...res.data.config.admin?.storage?.qiniu },
|
||||||
aliyun_oss: { ...config.value.admin.storage.aliyun_oss, ...res.data.config.admin?.storage?.aliyun_oss },
|
aliyun_oss: { ...config.value.admin.storage.aliyun_oss, ...res.data.config.admin?.storage?.aliyun_oss },
|
||||||
|
|
@ -216,15 +282,33 @@
|
||||||
email: { ...config.value.email, ...res.data.config.email },
|
email: { ...config.value.email, ...res.data.config.email },
|
||||||
data: {
|
data: {
|
||||||
database: { ...config.value.data.database, ...res.data.config.data?.database },
|
database: { ...config.value.data.database, ...res.data.config.data?.database },
|
||||||
redis: { ...config.value.data.redis, ...res.data.config.data?.redis }
|
redis: { ...config.value.data.redis, ...res.data.config.data?.redis },
|
||||||
|
database_list: res.data.config.data?.database_list || [],
|
||||||
|
redis_list: res.data.config.data?.redis_list || [],
|
||||||
|
mongo: { ...config.value.data.mongo, ...res.data.config.data?.mongo }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
redisClusterText.value = (config.value.data.redis.cluster_addrs || []).join('\n')
|
||||||
|
mongoHostsText.value = JSON.stringify(config.value.data.mongo.hosts || [], null, 2)
|
||||||
|
databaseListText.value = JSON.stringify(config.value.data.database_list || [], null, 2)
|
||||||
|
redisListText.value = JSON.stringify(config.value.data.redis_list || [], null, 2)
|
||||||
|
corsRulesText.value = JSON.stringify(config.value.admin.cors.whitelist || [], null, 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
try {
|
||||||
|
config.value.data.redis.cluster_addrs = redisClusterText.value.split('\n').map(item => item.trim()).filter(Boolean)
|
||||||
|
config.value.data.mongo.hosts = JSON.parse(mongoHostsText.value || '[]')
|
||||||
|
config.value.data.database_list = JSON.parse(databaseListText.value || '[]')
|
||||||
|
config.value.data.redis_list = JSON.parse(redisListText.value || '[]')
|
||||||
|
config.value.admin.cors.whitelist = JSON.parse(corsRulesText.value || '[]')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('MongoDB 或多数据库 JSON 格式不正确')
|
||||||
|
return
|
||||||
|
}
|
||||||
const res = await setSystemConfig({ config: config.value })
|
const res = await setSystemConfig({ config: config.value })
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
ElMessage.success('配置保存成功')
|
ElMessage.success('配置保存成功')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue