优化结构
This commit is contained in:
parent
b27a66e9d3
commit
9dd62c9fb6
13
cmd/main.go
13
cmd/main.go
|
|
@ -9,12 +9,16 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"kra/internal/app"
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
|
"kra/internal/server/router"
|
||||||
"kra/internal/service"
|
"kra/internal/service"
|
||||||
"kra/internal/service/dto"
|
"kra/internal/service/dto"
|
||||||
"kra/internal/worker"
|
"kra/internal/worker"
|
||||||
"kra/pkg/logging"
|
"kra/pkg/logging"
|
||||||
|
"kra/pkg/module"
|
||||||
"kra/pkg/mq"
|
"kra/pkg/mq"
|
||||||
|
platformtask "kra/pkg/task"
|
||||||
|
|
||||||
"github.com/go-kratos/kratos/v3"
|
"github.com/go-kratos/kratos/v3"
|
||||||
"github.com/go-kratos/kratos/v3/config"
|
"github.com/go-kratos/kratos/v3/config"
|
||||||
|
|
@ -41,6 +45,15 @@ func init() {
|
||||||
flag.StringVar(&flagconf, "conf", "./configs", "config path, eg: -conf config.yaml")
|
flag.StringVar(&flagconf, "conf", "./configs", "config path, eg: -conf config.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runtimeContributions is the binary-level list of modules with constructed
|
||||||
|
// route or task dependencies. Adding another runtime module is explicit here.
|
||||||
|
func runtimeContributions(systemRoutes *router.Routes, systemTasks *worker.TaskMethods) app.RuntimeContributions {
|
||||||
|
return app.RuntimeContributions{
|
||||||
|
Routes: []module.RouteRegistrar{systemRoutes},
|
||||||
|
Tasks: []platformtask.Contributor{systemTasks},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newApp(logger *slog.Logger, hs *kratoshttp.Server, scheduler *worker.TaskScheduler, audit *service.AuditRecorder, loggerControl *logging.ReloadableLogger, _ mq.Client) *kratos.App {
|
func newApp(logger *slog.Logger, hs *kratoshttp.Server, scheduler *worker.TaskScheduler, audit *service.AuditRecorder, loggerControl *logging.ReloadableLogger, _ mq.Client) *kratos.App {
|
||||||
if audit != nil && loggerControl != nil {
|
if audit != nil && loggerControl != nil {
|
||||||
loggerControl.SetErrorSink(logging.ErrorSinkFunc(func(ctx context.Context, entry logging.ErrorEntry) error {
|
loggerControl.SetErrorSink(logging.ErrorSinkFunc(func(ctx context.Context, entry logging.ErrorEntry) error {
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,8 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
|
||||||
tokenService := service.NewTokenService(tokenUsecase, tokenIssuer)
|
tokenService := service.NewTokenService(tokenUsecase, tokenIssuer)
|
||||||
apiToken := handler.NewAPIToken(tokenService)
|
apiToken := handler.NewAPIToken(tokenService)
|
||||||
initializationRepo := initialize.NewRepo(dataData, catalog)
|
initializationRepo := initialize.NewRepo(dataData, catalog)
|
||||||
systemConfigUsecase := system2.NewSystemConfigUsecase(initializationRepo, taskRuntime)
|
taskReloader := worker.NewTaskReloader(taskScheduler)
|
||||||
|
systemConfigUsecase := system2.NewSystemConfigUsecase(initializationRepo, taskReloader)
|
||||||
systemConfigService := service.NewSystemConfigService(systemConfigUsecase, runtimeSettings)
|
systemConfigService := service.NewSystemConfigService(systemConfigUsecase, runtimeSettings)
|
||||||
systemConfig := handler.NewSystemConfig(systemConfigService, securityService)
|
systemConfig := handler.NewSystemConfig(systemConfigService, securityService)
|
||||||
public := handler.NewPublic(authService, systemConfigService, securityService)
|
public := handler.NewPublic(authService, systemConfigService, securityService)
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -37,7 +37,6 @@ require (
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
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.mongodb.org/mongo-driver v1.17.2
|
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
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -411,8 +411,6 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
|
||||||
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/go.mod h1:dZuN/0sXeoscfWqsW8QLcLrGZdvsCC1B2R2CZ4kHmao=
|
|
||||||
go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
|
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.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=
|
||||||
|
|
@ -569,8 +567,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc=
|
|
||||||
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 h1:g0RAkxK/smSu/iRwC/KIX1mwUoVJtk2OjbgaeS4DmUM=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 h1:g0RAkxK/smSu/iRwC/KIX1mwUoVJtk2OjbgaeS4DmUM=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324/go.mod h1:Z4WJ5pJOYWFWcHEQUelD5QaZDknIQkpIL/+fyJOT9+A=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324/go.mod h1:Z4WJ5pJOYWFWcHEQUelD5QaZDknIQkpIL/+fyJOT9+A=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 h1:9HZDLIdYBJXAnaFOr9WHrKVycfpY+75s9HGadC0305A=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 h1:9HZDLIdYBJXAnaFOr9WHrKVycfpY+75s9HGadC0305A=
|
||||||
|
|
@ -605,8 +601,6 @@ gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
|
||||||
modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w=
|
modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w=
|
||||||
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
|
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
|
||||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProviderSet is biz providers.
|
// ProviderSet is biz providers.
|
||||||
var ProviderSet = wire.NewSet(system.NewUserUsecase, system.NewAuthenticationUsecase, system.NewSystemConfigUsecase, system.NewAuthorityUsecase, system.NewAPIUsecase, system.NewPermissionUsecase, system.NewAccessControlUsecase, system.NewMenuUsecase, system.NewDepartmentUsecase, system.NewPositionUsecase, system.NewDictionaryUsecase, system.NewParameterUsecase, system.NewTokenUsecase, system.NewSecurityUsecase, system.NewVersionUsecase, system.NewExportUsecase, system.NewAuditUsecase, system.NewAuditRecorderUsecase, system.NewLogViewerUsecase, system.NewTaskUsecaseWithRegistry, system.NewTaskApplicationUsecase, system.NewMediaUsecase, system.NewAnnouncementUsecase, system.NewEmailUsecase, system.NewPaymentUsecase, system.NewIntegrationConfigUsecase)
|
var ProviderSet = wire.NewSet(system.ProviderSet)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,9 @@ func NewAuthenticationUsecase(users *UserUsecase, security *SecurityUsecase, iss
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *AuthenticationUsecase) recordLogin(ctx context.Context, attempt *LoginAttempt, status bool, message string, userID uint) {
|
func (uc *AuthenticationUsecase) recordLogin(ctx context.Context, attempt *LoginAttempt, status bool, message string, userID uint) {
|
||||||
|
if uc.audit == nil || attempt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
_ = uc.audit.RecordLogin(ctx, &LoginLog{Username: attempt.Username, IP: attempt.IP, Status: status, ErrorMessage: message, Agent: attempt.Agent, UserID: userID})
|
_ = uc.audit.RecordLogin(ctx, &LoginLog{Username: attempt.Username, IP: attempt.IP, Status: status, ErrorMessage: message, Agent: attempt.Agent, UserID: userID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +68,7 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
||||||
if err != nil || config == nil {
|
if err != nil || config == nil {
|
||||||
return nil, fmt.Errorf("%w: security config unavailable", ErrLoginState)
|
return nil, fmt.Errorf("%w: security config unavailable", ErrLoginState)
|
||||||
}
|
}
|
||||||
if config != nil && config.LockEnable {
|
if config.LockEnable {
|
||||||
locked, lockErr := uc.security.LoginLocked(ctx, attempt.Username)
|
locked, lockErr := uc.security.LoginLocked(ctx, attempt.Username)
|
||||||
if lockErr != nil {
|
if lockErr != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrLoginState, lockErr)
|
return nil, fmt.Errorf("%w: %v", ErrLoginState, lockErr)
|
||||||
|
|
@ -77,17 +80,14 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
||||||
}
|
}
|
||||||
|
|
||||||
ipTTL := time.Hour
|
ipTTL := time.Hour
|
||||||
requireCaptcha := config == nil || config.CaptchaOpen == 0
|
if config.CaptchaTimeout > 0 {
|
||||||
if config != nil {
|
ipTTL = time.Duration(config.CaptchaTimeout) * time.Second
|
||||||
if config.CaptchaTimeout > 0 {
|
|
||||||
ipTTL = time.Duration(config.CaptchaTimeout) * time.Second
|
|
||||||
}
|
|
||||||
failures, counterErr := uc.security.EnsureLoginIPCounter(ctx, attempt.IP, ipTTL)
|
|
||||||
if counterErr != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", ErrLoginState, counterErr)
|
|
||||||
}
|
|
||||||
requireCaptcha = config.CaptchaOpen == 0 || failures > config.CaptchaOpen
|
|
||||||
}
|
}
|
||||||
|
failures, counterErr := uc.security.EnsureLoginIPCounter(ctx, attempt.IP, ipTTL)
|
||||||
|
if counterErr != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrLoginState, counterErr)
|
||||||
|
}
|
||||||
|
requireCaptcha := config.CaptchaOpen == 0 || failures > config.CaptchaOpen
|
||||||
if requireCaptcha && !uc.security.VerifyCaptcha(ctx, attempt.CaptchaID, attempt.Captcha, true) {
|
if requireCaptcha && !uc.security.VerifyCaptcha(ctx, attempt.CaptchaID, attempt.Captcha, true) {
|
||||||
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
||||||
uc.recordLogin(ctx, attempt, false, "验证码错误", 0)
|
uc.recordLogin(ctx, attempt, false, "验证码错误", 0)
|
||||||
|
|
@ -97,7 +97,7 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
||||||
user, err := uc.users.Login(ctx, attempt.Username, attempt.Password)
|
user, err := uc.users.Login(ctx, attempt.Username, attempt.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
||||||
if config != nil && config.LockEnable {
|
if config.LockEnable {
|
||||||
lockTTL := time.Duration(config.LockDuration) * time.Minute
|
lockTTL := time.Duration(config.LockDuration) * time.Minute
|
||||||
failures, stateErr := uc.security.IncrementLoginFailure(ctx, attempt.Username, lockTTL)
|
failures, stateErr := uc.security.IncrementLoginFailure(ctx, attempt.Username, lockTTL)
|
||||||
if stateErr != nil {
|
if stateErr != nil {
|
||||||
|
|
@ -119,7 +119,7 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
||||||
}
|
}
|
||||||
|
|
||||||
uc.security.ClearLoginState(ctx, attempt.Username)
|
uc.security.ClearLoginState(ctx, attempt.Username)
|
||||||
if config != nil && config.PwdExpireEnable && config.PwdExpireDays > 0 && user.PasswordUpdatedAt != nil && time.Now().After((*user.PasswordUpdatedAt).AddDate(0, 0, config.PwdExpireDays)) {
|
if config.PwdExpireEnable && config.PwdExpireDays > 0 && user.PasswordUpdatedAt != nil && time.Now().After((*user.PasswordUpdatedAt).AddDate(0, 0, config.PwdExpireDays)) {
|
||||||
user.MustChangePassword = true
|
user.MustChangePassword = true
|
||||||
}
|
}
|
||||||
issued, err := uc.issuer.IssueToken(user, user.AuthorityID, user.MustChangePassword, 0)
|
issued, err := uc.issuer.IssueToken(user, user.AuthorityID, user.MustChangePassword, 0)
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,11 @@ func TestLoginRecordsSuccessBeforeMultipointCacheFailure(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRecordLoginAllowsMissingAuditRecorder(t *testing.T) {
|
||||||
|
uc := &AuthenticationUsecase{}
|
||||||
|
uc.recordLogin(context.Background(), &LoginAttempt{Username: "admin"}, false, "failed", 0)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSwitchAuthorityReissuesCurrentClaimsWithoutReloadingUser(t *testing.T) {
|
func TestSwitchAuthorityReissuesCurrentClaimsWithoutReloadingUser(t *testing.T) {
|
||||||
repo := &switchAuthorityUserRepo{}
|
repo := &switchAuthorityUserRepo{}
|
||||||
issuer := &switchAuthorityIssuer{}
|
issuer := &switchAuthorityIssuer{}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go.einride.tech/aip/filtering"
|
|
||||||
"go.einride.tech/aip/ordering"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ListOption func(*ListOptions)
|
|
||||||
|
|
||||||
type ListOptions struct {
|
|
||||||
Filter filtering.Filter
|
|
||||||
OrderBy ordering.OrderBy
|
|
||||||
Offset int
|
|
||||||
Limit int
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListFilter(filter filtering.Filter) ListOption {
|
|
||||||
return func(o *ListOptions) {
|
|
||||||
o.Filter = filter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListOrderBy(orderBy ordering.OrderBy) ListOption {
|
|
||||||
return func(o *ListOptions) {
|
|
||||||
o.OrderBy = orderBy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListOffset(offset int) ListOption {
|
|
||||||
return func(o *ListOptions) {
|
|
||||||
o.Offset = offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListLimit(limit int) ListOption {
|
|
||||||
return func(o *ListOptions) {
|
|
||||||
o.Limit = limit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import "github.com/google/wire"
|
||||||
|
|
||||||
|
// ProviderSet wires the system domain usecases. The parent biz package keeps
|
||||||
|
// the aggregate entry point used by Wire while each domain owns its providers.
|
||||||
|
var ProviderSet = wire.NewSet(
|
||||||
|
NewUserUsecase,
|
||||||
|
NewAuthenticationUsecase,
|
||||||
|
NewSystemConfigUsecase,
|
||||||
|
NewAuthorityUsecase,
|
||||||
|
NewAPIUsecase,
|
||||||
|
NewPermissionUsecase,
|
||||||
|
NewAccessControlUsecase,
|
||||||
|
NewMenuUsecase,
|
||||||
|
NewDepartmentUsecase,
|
||||||
|
NewPositionUsecase,
|
||||||
|
NewDictionaryUsecase,
|
||||||
|
NewParameterUsecase,
|
||||||
|
NewTokenUsecase,
|
||||||
|
NewSecurityUsecase,
|
||||||
|
NewVersionUsecase,
|
||||||
|
NewExportUsecase,
|
||||||
|
NewAuditUsecase,
|
||||||
|
NewAuditRecorderUsecase,
|
||||||
|
NewLogViewerUsecase,
|
||||||
|
NewTaskUsecaseWithRegistry,
|
||||||
|
NewTaskApplicationUsecase,
|
||||||
|
NewMediaUsecase,
|
||||||
|
NewAnnouncementUsecase,
|
||||||
|
NewEmailUsecase,
|
||||||
|
NewPaymentUsecase,
|
||||||
|
NewIntegrationConfigUsecase,
|
||||||
|
)
|
||||||
|
|
@ -30,12 +30,16 @@ type InitializationRepo interface {
|
||||||
DiskMountPoints() []string
|
DiskMountPoints() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemConfigUsecase struct {
|
type TaskReloader interface {
|
||||||
repo InitializationRepo
|
Reload(context.Context) error
|
||||||
tasks TaskRuntime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSystemConfigUsecase(repo InitializationRepo, tasks TaskRuntime) *SystemConfigUsecase {
|
type SystemConfigUsecase struct {
|
||||||
|
repo InitializationRepo
|
||||||
|
tasks TaskReloader
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSystemConfigUsecase(repo InitializationRepo, tasks TaskReloader) *SystemConfigUsecase {
|
||||||
return &SystemConfigUsecase{repo: repo, tasks: tasks}
|
return &SystemConfigUsecase{repo: repo, tasks: tasks}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ func NewUserUsecase(repo UserRepo) *UserUsecase { return &UserUsecase{repo: repo
|
||||||
|
|
||||||
func (uc *UserUsecase) Login(ctx context.Context, username, password string) (*User, error) {
|
func (uc *UserUsecase) Login(ctx context.Context, username, password string) (*User, error) {
|
||||||
u, err := uc.repo.FindUserByUsername(ctx, username)
|
u, err := uc.repo.FindUserByUsername(ctx, username)
|
||||||
if err != nil || bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) != nil {
|
if err != nil || u == nil || bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) != nil {
|
||||||
return nil, ErrInvalidCredentials
|
return nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
uc.fallbackDefaultRouter(ctx, u)
|
uc.fallbackDefaultRouter(ctx, u)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,16 @@ type defaultRouterUserRepo struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type loginUserRepo struct {
|
||||||
|
UserRepo
|
||||||
|
user *User
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *loginUserRepo) FindUserByUsername(context.Context, string) (*User, error) {
|
||||||
|
return r.user, r.err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *defaultRouterUserRepo) HasAuthorityMenu(context.Context, uint, string) (bool, error) {
|
func (r *defaultRouterUserRepo) HasAuthorityMenu(context.Context, uint, string) (bool, error) {
|
||||||
return r.hasMenu, r.err
|
return r.hasMenu, r.err
|
||||||
}
|
}
|
||||||
|
|
@ -35,3 +45,10 @@ func TestFallbackDefaultRouterMatchesMenuLookupOutcome(t *testing.T) {
|
||||||
t.Fatalf("empty missing default route = %q", user.Authority.DefaultRouter)
|
t.Fatalf("empty missing default route = %q", user.Authority.DefaultRouter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoginRejectsMissingUserWithoutPanic(t *testing.T) {
|
||||||
|
user, err := NewUserUsecase(&loginUserRepo{}).Login(context.Background(), "missing", "password")
|
||||||
|
if !errors.Is(err, ErrInvalidCredentials) || user != nil {
|
||||||
|
t.Fatalf("Login() = %#v, %v; want nil, ErrInvalidCredentials", user, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ watching, and repository implementations.
|
||||||
|
|
||||||
- `repository/`: system repositories and table persistence
|
- `repository/`: system repositories and table persistence
|
||||||
- `payment/`: payment configuration and payment-order persistence
|
- `payment/`: payment configuration and payment-order persistence
|
||||||
|
- each subpackage owns its Wire `ProviderSet`; the root package only binds the
|
||||||
|
shared `Data` infrastructure and aggregates those sets
|
||||||
- root files: shared database lifecycle, runtime clients, integration-config
|
- root files: shared database lifecycle, runtime clients, integration-config
|
||||||
storage, data-scope auditing, and migration orchestration
|
storage, data-scope auditing, and migration orchestration
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,8 @@ var ProviderSet = wire.NewSet(
|
||||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||||
wire.Bind(new(datapayment.Provider), new(*Data)),
|
wire.Bind(new(datapayment.Provider), new(*Data)),
|
||||||
datasystem.NewRuntimeSettings,
|
datasystem.ProviderSet,
|
||||||
datasystem.NewTokenIssuer,
|
datapayment.ProviderSet,
|
||||||
datasystem.NewUserRepo, datasystem.NewAuthorityAccessRepo, datasystem.NewAPIRepo, datasystem.NewPermissionRepo,
|
|
||||||
datasystem.NewMenuRepo, datasystem.NewDepartmentRepo, datasystem.NewPositionRepo, datasystem.NewDictionaryRepo, datasystem.NewParameterRepo, datasystem.NewAPITokenRepo,
|
|
||||||
datasystem.NewSecurityRepo,
|
|
||||||
datasystem.NewVersionRepo, datasystem.NewExportRepo, datasystem.NewAuditRepo, datasystem.NewAuditRecorderRepo, datasystem.NewLogFileRepo, datasystem.NewTaskRepo,
|
|
||||||
datasystem.NewMediaRepo, datasystem.NewAnnouncementRepo, datapayment.NewPaymentRepo, datapayment.NewPaymentOrderRepo,
|
|
||||||
datasystem.NewIntegrationConfigRepo,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewIntegrationRuntime(data *Data) *runtimeconfig.Store {
|
func NewIntegrationRuntime(data *Data) *runtimeconfig.Store {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package payment
|
||||||
|
|
||||||
|
import "github.com/google/wire"
|
||||||
|
|
||||||
|
// ProviderSet wires payment persistence repositories.
|
||||||
|
var ProviderSet = wire.NewSet(NewPaymentRepo, NewPaymentOrderRepo)
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import "github.com/google/wire"
|
||||||
|
|
||||||
|
// ProviderSet wires system repositories and their runtime-backed adapters.
|
||||||
|
var ProviderSet = wire.NewSet(
|
||||||
|
NewRuntimeSettings,
|
||||||
|
NewTokenIssuer,
|
||||||
|
NewUserRepo,
|
||||||
|
NewAuthorityAccessRepo,
|
||||||
|
NewAPIRepo,
|
||||||
|
NewPermissionRepo,
|
||||||
|
NewMenuRepo,
|
||||||
|
NewDepartmentRepo,
|
||||||
|
NewPositionRepo,
|
||||||
|
NewDictionaryRepo,
|
||||||
|
NewParameterRepo,
|
||||||
|
NewAPITokenRepo,
|
||||||
|
NewSecurityRepo,
|
||||||
|
NewVersionRepo,
|
||||||
|
NewExportRepo,
|
||||||
|
NewAuditRepo,
|
||||||
|
NewAuditRecorderRepo,
|
||||||
|
NewLogFileRepo,
|
||||||
|
NewTaskRepo,
|
||||||
|
NewMediaRepo,
|
||||||
|
NewAnnouncementRepo,
|
||||||
|
NewIntegrationConfigRepo,
|
||||||
|
)
|
||||||
|
|
@ -27,6 +27,9 @@ func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, h
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessControlService, auth middleware.TokenAuthenticator, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string, routes *platformmodule.Runtime, ws *websocket.Server) *gin.Engine {
|
func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessControlService, auth middleware.TokenAuthenticator, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string, routes *platformmodule.Runtime, ws *websocket.Server) *gin.Engine {
|
||||||
|
if runtime == nil {
|
||||||
|
runtime = conf.NewRuntime(nil, nil)
|
||||||
|
}
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
if err := engine.SetTrustedProxies(nil); err != nil && logger != nil {
|
if err := engine.SetTrustedProxies(nil); err != nil && logger != nil {
|
||||||
|
|
|
||||||
|
|
@ -232,15 +232,24 @@ func failLogViewer(c *gin.Context, err error, logger *slog.Logger) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Audit) DeleteError(c *gin.Context) {
|
func (h *Audit) DeleteError(c *gin.Context) {
|
||||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
id, err := positiveUintQuery(c, "ID")
|
||||||
if err := h.service.DeleteErrors(c.Request.Context(), []uint{uint(id)}); err != nil {
|
if err != nil {
|
||||||
|
Fail(c, "错误记录ID非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.service.DeleteErrors(c.Request.Context(), []uint{id}); err != nil {
|
||||||
Fail(c, "删除失败:"+err.Error())
|
Fail(c, "删除失败:"+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Write(c, CodeSuccess, gin.H{}, "删除成功")
|
Write(c, CodeSuccess, gin.H{}, "删除成功")
|
||||||
}
|
}
|
||||||
func (h *Audit) DeleteErrors(c *gin.Context) {
|
func (h *Audit) DeleteErrors(c *gin.Context) {
|
||||||
if err := h.service.DeleteErrors(c.Request.Context(), IDsFromQuery(c)); err != nil {
|
ids, err := IDsFromQuery(c)
|
||||||
|
if err != nil {
|
||||||
|
Fail(c, "错误记录ID非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.service.DeleteErrors(c.Request.Context(), ids); err != nil {
|
||||||
Fail(c, "批量删除失败:"+err.Error())
|
Fail(c, "批量删除失败:"+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -259,8 +268,12 @@ func (h *Audit) UpdateError(c *gin.Context) {
|
||||||
Write(c, CodeSuccess, gin.H{}, "更新成功")
|
Write(c, CodeSuccess, gin.H{}, "更新成功")
|
||||||
}
|
}
|
||||||
func (h *Audit) Error(c *gin.Context) {
|
func (h *Audit) Error(c *gin.Context) {
|
||||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
id, err := positiveUintQuery(c, "ID")
|
||||||
item, err := h.service.Error(c.Request.Context(), uint(id))
|
if err != nil {
|
||||||
|
Fail(c, "错误记录ID非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := h.service.Error(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Fail(c, "查询失败:"+err.Error())
|
Fail(c, "查询失败:"+err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,11 @@ func (h *Export) Preview(c *gin.Context) {
|
||||||
Fail(c, "模板ID不能为空")
|
Fail(c, "模板ID不能为空")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
params, _ := exportParams(c.Request.URL.Query())
|
params, err := exportParams(c.Request.URL.Query())
|
||||||
|
if err != nil {
|
||||||
|
Fail(c, "解析 params 参数失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
sql, err := h.service.Preview(c.Request.Context(), templateID, params)
|
sql, err := h.service.Preview(c.Request.Context(), templateID, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Fail(c, "获取失败")
|
Fail(c, "获取失败")
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,11 @@ func (h *Media) Upload(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer opened.Close()
|
defer opened.Close()
|
||||||
category, _ := strconv.Atoi(c.DefaultPostForm("classId", "0"))
|
category, parseErr := strconv.Atoi(c.DefaultPostForm("classId", "0"))
|
||||||
|
if parseErr != nil || category < 0 {
|
||||||
|
Fail(c, "文件分类 ID 非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
save := c.DefaultQuery("noSave", "0") == "0"
|
save := c.DefaultQuery("noSave", "0") == "0"
|
||||||
item, err := h.service.Upload(c.Request.Context(), claims.ID, header.Filename, header.Header.Get("Content-Type"), category, opened, save)
|
item, err := h.service.Upload(c.Request.Context(), claims.ID, header.Filename, header.Header.Get("Content-Type"), category, opened, save)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -276,12 +280,16 @@ func (h *Media) CompleteUpload(c *gin.Context) {
|
||||||
}
|
}
|
||||||
func (h *Media) CancelUpload(c *gin.Context) {
|
func (h *Media) CancelUpload(c *gin.Context) {
|
||||||
claims := Claims(c)
|
claims := Claims(c)
|
||||||
id, _ := strconv.ParseUint(c.Param("uploadId"), 10, 64)
|
|
||||||
if claims == nil {
|
if claims == nil {
|
||||||
Fail(c, "未登录")
|
Fail(c, "未登录")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.service.CancelUpload(c.Request.Context(), claims.ID, uint(id)); err != nil {
|
id, err := positiveUintParam(c, "uploadId")
|
||||||
|
if err != nil {
|
||||||
|
Fail(c, "上传会话 ID 非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.service.CancelUpload(c.Request.Context(), claims.ID, id); err != nil {
|
||||||
Fail(c, err.Error())
|
Fail(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,31 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errInvalidPositiveUint = errors.New("参数必须是正整数")
|
||||||
|
|
||||||
|
func positiveUint(raw string) (uint, error) {
|
||||||
|
parsed, err := strconv.ParseUint(strings.TrimSpace(raw), 10, 64)
|
||||||
|
if err != nil || parsed == 0 || uint64(uint(parsed)) != parsed {
|
||||||
|
return 0, errInvalidPositiveUint
|
||||||
|
}
|
||||||
|
return uint(parsed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func positiveUintQuery(c *gin.Context, key string) (uint, error) {
|
||||||
|
return positiveUint(c.Query(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func positiveUintParam(c *gin.Context, key string) (uint, error) {
|
||||||
|
return positiveUint(c.Param(key))
|
||||||
|
}
|
||||||
|
|
||||||
func page(c *gin.Context) (int, int, error) {
|
func page(c *gin.Context) (int, int, error) {
|
||||||
var value, size int
|
var value, size int
|
||||||
if raw, exists := c.GetQuery("page"); exists && raw != "" {
|
if raw, exists := c.GetQuery("page"); exists && raw != "" {
|
||||||
|
|
@ -25,14 +45,18 @@ func page(c *gin.Context) (int, int, error) {
|
||||||
return value, size, nil
|
return value, size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func IDsFromQuery(c *gin.Context) []uint {
|
func IDsFromQuery(c *gin.Context) ([]uint, error) {
|
||||||
values := c.QueryArray("IDs[]")
|
values := c.QueryArray("IDs[]")
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil, errInvalidPositiveUint
|
||||||
|
}
|
||||||
ids := make([]uint, 0, len(values))
|
ids := make([]uint, 0, len(values))
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
id, _ := strconv.ParseUint(value, 10, 64)
|
id, err := positiveUint(value)
|
||||||
if id > 0 {
|
if err != nil {
|
||||||
ids = append(ids, uint(id))
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
return ids
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"kra/internal/server/middleware"
|
||||||
"kra/internal/service"
|
"kra/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
@ -10,9 +11,10 @@ type Session struct{ tokens *service.TokenService }
|
||||||
|
|
||||||
func NewSession(tokens *service.TokenService) *Session { return &Session{tokens: tokens} }
|
func NewSession(tokens *service.TokenService) *Session { return &Session{tokens: tokens} }
|
||||||
func (h *Session) Logout(c *gin.Context) {
|
func (h *Session) Logout(c *gin.Context) {
|
||||||
token := c.GetHeader("x-token")
|
token := middleware.RequestToken(c, false)
|
||||||
if token == "" {
|
if token == "" {
|
||||||
token, _ = c.Cookie("x-token")
|
NoAuth(c, "未登录或非法访问")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := h.tokens.BlacklistToken(c.Request.Context(), token); err != nil {
|
if err := h.tokens.BlacklistToken(c.Request.Context(), token); err != nil {
|
||||||
Fail(c, "jwt作废失败")
|
Fail(c, "jwt作废失败")
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,12 @@ func versionStage(err error) (system.VersionStage, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Version) Delete(c *gin.Context) {
|
func (h *Version) Delete(c *gin.Context) {
|
||||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
id, err := positiveUintQuery(c, "ID")
|
||||||
if err := h.service.DeleteVersions(c.Request.Context(), []uint{uint(id)}); err != nil {
|
if err != nil {
|
||||||
|
Fail(c, "版本ID非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.service.DeleteVersions(c.Request.Context(), []uint{id}); err != nil {
|
||||||
Fail(c, "删除失败:"+err.Error())
|
Fail(c, "删除失败:"+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -38,8 +42,16 @@ func (h *Version) DeleteMany(c *gin.Context) {
|
||||||
raw := c.QueryArray("IDs[]")
|
raw := c.QueryArray("IDs[]")
|
||||||
ids := make([]uint, 0, len(raw))
|
ids := make([]uint, 0, len(raw))
|
||||||
for _, value := range raw {
|
for _, value := range raw {
|
||||||
id, _ := strconv.ParseUint(value, 10, 64)
|
id, err := positiveUint(value)
|
||||||
ids = append(ids, uint(id))
|
if err != nil {
|
||||||
|
Fail(c, "版本ID非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
Fail(c, "版本ID不能为空")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := h.service.DeleteVersions(c.Request.Context(), ids); err != nil {
|
if err := h.service.DeleteVersions(c.Request.Context(), ids); err != nil {
|
||||||
Fail(c, "批量删除失败:"+err.Error())
|
Fail(c, "批量删除失败:"+err.Error())
|
||||||
|
|
@ -48,8 +60,12 @@ func (h *Version) DeleteMany(c *gin.Context) {
|
||||||
Write(c, CodeSuccess, gin.H{}, "批量删除成功")
|
Write(c, CodeSuccess, gin.H{}, "批量删除成功")
|
||||||
}
|
}
|
||||||
func (h *Version) Find(c *gin.Context) {
|
func (h *Version) Find(c *gin.Context) {
|
||||||
id, _ := strconv.ParseUint(c.Query("ID"), 10, 64)
|
id, err := positiveUintQuery(c, "ID")
|
||||||
item, err := h.service.Version(c.Request.Context(), uint(id))
|
if err != nil {
|
||||||
|
Fail(c, "版本ID非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
item, err := h.service.Version(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Fail(c, "查询失败:"+err.Error())
|
Fail(c, "查询失败:"+err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -112,8 +128,12 @@ func (h *Version) Download(c *gin.Context) {
|
||||||
Fail(c, "版本ID不能为空")
|
Fail(c, "版本ID不能为空")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id, _ := strconv.ParseUint(rawID, 10, 64)
|
id, err := positiveUint(rawID)
|
||||||
raw, code, err := h.service.VersionData(c.Request.Context(), uint(id))
|
if err != nil {
|
||||||
|
Fail(c, "版本ID非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
raw, code, err := h.service.VersionData(c.Request.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Fail(c, "获取版本记录失败:"+err.Error())
|
Fail(c, "获取版本记录失败:"+err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ func AccessControl(runtime *conf.Runtime, access accessController) gin.HandlerFu
|
||||||
NoAuth(c, "未登录或非法访问")
|
NoAuth(c, "未登录或非法访问")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if access == nil {
|
||||||
|
Write(c, CodeError, gin.H{}, "权限服务不可用")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
policyPath := path
|
policyPath := path
|
||||||
if config := runtime.Admin(); config != nil {
|
if config := runtime.Admin(); config != nil {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.H
|
||||||
var requestBody []byte
|
var requestBody []byte
|
||||||
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
|
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
|
||||||
mediaUpload := multipart && isMediaUploadRoute(c.FullPath())
|
mediaUpload := multipart && isMediaUploadRoute(c.FullPath())
|
||||||
config := runtime.Admin()
|
var config *conf.AdminBackend
|
||||||
|
if runtime != nil {
|
||||||
|
config = runtime.Admin()
|
||||||
|
}
|
||||||
bodyLimit := defaultRequestBodyLimit
|
bodyLimit := defaultRequestBodyLimit
|
||||||
if mediaUpload {
|
if mediaUpload {
|
||||||
bodyLimit = system.DefaultMaxMediaFileSize + (1 << 20)
|
bodyLimit = system.DefaultMaxMediaFileSize + (1 << 20)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@ const ctxOperationAuditPersistFailedKey = "operation_audit_persist_failed"
|
||||||
|
|
||||||
func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.HandlerFunc {
|
func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
if runtime == nil || recorder == nil {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
if !recordsOperation(c.Request.Method, path) {
|
if !recordsOperation(c.Request.Method, path) {
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
@ -95,12 +99,11 @@ func OperationAudit(runtime *conf.Runtime, recorder *service.AuditRecorder) gin.
|
||||||
}
|
}
|
||||||
|
|
||||||
func operationQueryBody(raw string) []byte {
|
func operationQueryBody(raw string) []byte {
|
||||||
query, _ := url.QueryUnescape(raw)
|
parsed, _ := url.ParseQuery(raw)
|
||||||
values := make(map[string]string)
|
values := make(map[string]string, len(parsed))
|
||||||
for _, item := range strings.Split(query, "&") {
|
for key, items := range parsed {
|
||||||
parts := strings.Split(item, "=")
|
if len(items) > 0 {
|
||||||
if len(parts) == 2 {
|
values[key] = items[len(items)-1]
|
||||||
values[parts[0]] = parts[1]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(&values)
|
body, _ := json.Marshal(&values)
|
||||||
|
|
@ -189,23 +192,6 @@ func operationPathMatches(pattern, path string) bool {
|
||||||
return len(patternParts) == len(pathParts)
|
return len(patternParts) == len(pathParts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func routeSuffix(path string) string {
|
|
||||||
bestIndex := -1
|
|
||||||
bestPath := path
|
|
||||||
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/", "/integration/", "/payment/"} {
|
|
||||||
if index := strings.Index(path, marker); index >= 0 {
|
|
||||||
// Router prefixes may themselves contain a registered route marker
|
|
||||||
// (for example /api/integration/...). Keep the deepest match so the
|
|
||||||
// policy and audit route remain the actual application endpoint.
|
|
||||||
if index > bestIndex {
|
|
||||||
bestIndex = index
|
|
||||||
bestPath = path[index:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bestPath
|
|
||||||
}
|
|
||||||
|
|
||||||
var operationRoutes = func() map[string]struct{} {
|
var operationRoutes = func() map[string]struct{} {
|
||||||
values := []string{
|
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",
|
"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",
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,9 @@ func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestToken(c *gin.Context, allowQueryToken bool) string {
|
// RequestToken extracts the authentication token accepted by HTTP handlers.
|
||||||
|
// Query-string tokens are opt-in for WebSocket handshakes only.
|
||||||
|
func RequestToken(c *gin.Context, allowQueryToken bool) string {
|
||||||
token := strings.TrimSpace(c.GetHeader("x-token"))
|
token := strings.TrimSpace(c.GetHeader("x-token"))
|
||||||
if token == "" {
|
if token == "" {
|
||||||
authorization := strings.TrimSpace(c.GetHeader("Authorization"))
|
authorization := strings.TrimSpace(c.GetHeader("Authorization"))
|
||||||
|
|
@ -91,6 +93,10 @@ func requestToken(c *gin.Context, allowQueryToken bool) string {
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requestToken(c *gin.Context, allowQueryToken bool) string {
|
||||||
|
return RequestToken(c, allowQueryToken)
|
||||||
|
}
|
||||||
|
|
||||||
func tokenErrorMessage(err error) string {
|
func tokenErrorMessage(err error) string {
|
||||||
message := "无法处理此token"
|
message := "无法处理此token"
|
||||||
switch {
|
switch {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ const (
|
||||||
// configuration reload takes effect without rebuilding the Gin engine.
|
// configuration reload takes effect without rebuilding the Gin engine.
|
||||||
func CORS(runtime *conf.Runtime) gin.HandlerFunc {
|
func CORS(runtime *conf.Runtime) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
if runtime == nil {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
config := runtime.Admin()
|
config := runtime.Admin()
|
||||||
if config == nil || config.Cors == nil {
|
if config == nil || config.Cors == nil {
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
@ -28,12 +32,14 @@ func CORS(runtime *conf.Runtime) gin.HandlerFunc {
|
||||||
origin := c.GetHeader("Origin")
|
origin := c.GetHeader("Origin")
|
||||||
corsHandled := false
|
corsHandled := false
|
||||||
if mode == "allow-all" {
|
if mode == "allow-all" {
|
||||||
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true)
|
if origin != "" {
|
||||||
corsHandled = true
|
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true)
|
||||||
|
corsHandled = true
|
||||||
|
}
|
||||||
} else if rule := matchingCORSRule(config.Cors.Whitelist, origin); rule != nil {
|
} else if rule := matchingCORSRule(config.Cors.Whitelist, origin); rule != nil {
|
||||||
setCORSHeaders(c, rule.AllowOrigin, rule.AllowHeaders, rule.AllowMethods, rule.ExposeHeaders, rule.AllowCredentials)
|
setCORSHeaders(c, rule.AllowOrigin, rule.AllowHeaders, rule.AllowMethods, rule.ExposeHeaders, rule.AllowCredentials)
|
||||||
corsHandled = true
|
corsHandled = true
|
||||||
} else if mode == "strict-whitelist" && !(c.Request.Method == http.MethodGet && c.Request.URL.Path == "/health") {
|
} else if mode == "strict-whitelist" && !(c.Request.Method == http.MethodGet && isHealthPath(c.Request.URL.Path)) {
|
||||||
c.AbortWithStatus(http.StatusForbidden)
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +51,11 @@ func CORS(runtime *conf.Runtime) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isHealthPath(path string) bool {
|
||||||
|
path = strings.TrimSuffix(path, "/")
|
||||||
|
return path == "/health" || strings.HasSuffix(path, "/health")
|
||||||
|
}
|
||||||
|
|
||||||
func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.AdminBackend_CORSRule {
|
func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.AdminBackend_CORSRule {
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if rule != nil && origin == rule.AllowOrigin {
|
if rule != nil && origin == rule.AllowOrigin {
|
||||||
|
|
@ -55,6 +66,7 @@ func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCORSHeaders(c *gin.Context, origin, headers, methods, expose string, credentials bool) {
|
func setCORSHeaders(c *gin.Context, origin, headers, methods, expose string, credentials bool) {
|
||||||
|
c.Header("Vary", "Origin")
|
||||||
c.Header("Access-Control-Allow-Origin", origin)
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
c.Header("Access-Control-Allow-Headers", headers)
|
c.Header("Access-Control-Allow-Headers", headers)
|
||||||
c.Header("Access-Control-Allow-Methods", methods)
|
c.Header("Access-Control-Allow-Methods", methods)
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,9 @@ func TestCORSConsumesMatchedWhitelistPreflight(t *testing.T) {
|
||||||
t.Fatalf("matched whitelist preflight status = %d, want %d", response.Code, http.StatusNoContent)
|
t.Fatalf("matched whitelist preflight status = %d, want %d", response.Code, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCORSStrictWhitelistAllowsPrefixedHealth(t *testing.T) {
|
||||||
|
if !isHealthPath("/admin/health") {
|
||||||
|
t.Fatal("prefixed health endpoint was not recognized by strict whitelist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -12,6 +13,16 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestOperationQueryBodyPreservesEncodedSeparators(t *testing.T) {
|
||||||
|
var values map[string]string
|
||||||
|
if err := json.Unmarshal(operationQueryBody("filter=one%3Dtwo%26three&repeat=first&repeat=last"), &values); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if values["filter"] != "one=two&three" || values["repeat"] != "last" {
|
||||||
|
t.Fatalf("unexpected query body: %#v", values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func runErrorAudit(t *testing.T, path, response string, logger *slog.Logger) {
|
func runErrorAudit(t *testing.T, path, response string, logger *slog.Logger) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if settings == nil {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
config, err := settings.CurrentSecurity(c.Request.Context())
|
config, err := settings.CurrentSecurity(c.Request.Context())
|
||||||
if err != nil || config == nil {
|
if err != nil || config == nil {
|
||||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
|
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
|
||||||
|
|
@ -29,7 +33,11 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
|
||||||
if window < 1 {
|
if window < 1 {
|
||||||
window = 60
|
window = 60
|
||||||
}
|
}
|
||||||
key := "KRA_SecLimit" + c.ClientIP() + c.FullPath()
|
route := c.FullPath()
|
||||||
|
if route == "" {
|
||||||
|
route = path
|
||||||
|
}
|
||||||
|
key := "KRA_SecLimit:" + c.ClientIP() + ":" + route
|
||||||
count, cacheErr := settings.IncrementRateLimit(c.Request.Context(), key, time.Duration(window)*time.Second)
|
count, cacheErr := settings.IncrementRateLimit(c.Request.Context(), key, time.Duration(window)*time.Second)
|
||||||
if cacheErr != nil {
|
if cacheErr != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
|
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ func registerSwagger(engine *gin.Engine, prefix, version string, logger *slog.Lo
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string {
|
func buildSwaggerDocument(routes []gin.RouteInfo, prefix, version string) string {
|
||||||
|
routes = append([]gin.RouteInfo(nil), routes...)
|
||||||
basePath := strings.TrimSuffix(prefix, "/")
|
basePath := strings.TrimSuffix(prefix, "/")
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
basePath = "/"
|
basePath = "/"
|
||||||
|
|
@ -159,8 +160,8 @@ func swaggerPathParameters(path string) []map[string]any {
|
||||||
func swaggerPublicPath(path string) bool {
|
func swaggerPublicPath(path string) bool {
|
||||||
for _, marker := range []string{
|
for _, marker := range []string{
|
||||||
"/health", "/base/login", "/base/captcha", "/init/checkdb", "/init/initdb",
|
"/health", "/base/login", "/base/captcha", "/init/checkdb", "/init/initdb",
|
||||||
"/api/freshCasbin", "/sysExportTemplate/exportExcelByToken", "/sysExportTemplate/exportTemplateByToken",
|
"/sysExportTemplate/exportExcelByToken", "/sysExportTemplate/exportTemplateByToken",
|
||||||
"/sysError/createSysError", "/info/getInfoDataSource", "/info/getInfoPublic",
|
"/sysError/createSysError", "/info/getInfoPublic",
|
||||||
} {
|
} {
|
||||||
if path == marker {
|
if path == marker {
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSwaggerMarksOnlyPublicRoutesWithoutAuth(t *testing.T) {
|
||||||
|
document := buildSwaggerDocument([]gin.RouteInfo{
|
||||||
|
{Method: "GET", Path: "/api/freshCasbin"},
|
||||||
|
{Method: "GET", Path: "/info/getInfoDataSource"},
|
||||||
|
{Method: "GET", Path: "/info/getInfoPublic"},
|
||||||
|
}, "", "test")
|
||||||
|
var payload struct {
|
||||||
|
Paths map[string]map[string]map[string]any `json:"paths"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(document), &payload); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, path := range []string{"/api/freshCasbin", "/info/getInfoDataSource"} {
|
||||||
|
if _, ok := payload.Paths[path]["get"]["security"]; !ok {
|
||||||
|
t.Fatalf("private route %s was marked public", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := payload.Paths["/info/getInfoPublic"]["get"]["security"]; ok {
|
||||||
|
t.Fatal("public route /info/getInfoPublic requires authentication")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,8 @@ func NewTaskScheduler(tasks *system.TaskUsecase, authorities *system.AuthorityUs
|
||||||
|
|
||||||
func NewTaskRuntime(scheduler *TaskScheduler) system.TaskRuntime { return scheduler }
|
func NewTaskRuntime(scheduler *TaskScheduler) system.TaskRuntime { return scheduler }
|
||||||
|
|
||||||
|
func NewTaskReloader(scheduler *TaskScheduler) system.TaskReloader { return scheduler }
|
||||||
|
|
||||||
func (s *TaskScheduler) Start(ctx context.Context) error {
|
func (s *TaskScheduler) Start(ctx context.Context) error {
|
||||||
runContext, cancel := context.WithCancel(ctx)
|
runContext, cancel := context.WithCancel(ctx)
|
||||||
s.ctxMu.Lock()
|
s.ctxMu.Lock()
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ package worker
|
||||||
import "github.com/google/wire"
|
import "github.com/google/wire"
|
||||||
|
|
||||||
// ProviderSet contains background task runtime providers.
|
// ProviderSet contains background task runtime providers.
|
||||||
var ProviderSet = wire.NewSet(NewTaskMethods, NewTaskExecutorWithRegistry, NewTaskScheduler, NewTaskRuntime)
|
var ProviderSet = wire.NewSet(NewTaskMethods, NewTaskExecutorWithRegistry, NewTaskScheduler, NewTaskRuntime, NewTaskReloader)
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@ func (r *Runtime) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.E
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, registrar := range r.routes {
|
for _, registrar := range r.routes {
|
||||||
|
if registrar == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
registrar.RegisterRoutes(public, private, engine)
|
registrar.RegisterRoutes(public, private, engine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"kra/pkg/database/migration"
|
"kra/pkg/database/migration"
|
||||||
"kra/pkg/task"
|
"kra/pkg/task"
|
||||||
)
|
)
|
||||||
|
|
@ -41,3 +42,18 @@ func TestCatalogCollectsContributions(t *testing.T) {
|
||||||
t.Fatalf("unexpected task methods: %#v", got)
|
t.Fatalf("unexpected task methods: %#v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type routeRegistrarStub struct{ called bool }
|
||||||
|
|
||||||
|
func (stub *routeRegistrarStub) RegisterRoutes(*gin.RouterGroup, *gin.RouterGroup, *gin.Engine) {
|
||||||
|
stub.called = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuntimeSkipsNilRouteRegistrars(t *testing.T) {
|
||||||
|
stub := &routeRegistrarStub{}
|
||||||
|
runtime := NewRuntime(nil, stub)
|
||||||
|
runtime.RegisterRoutes(nil, nil, nil)
|
||||||
|
if !stub.called {
|
||||||
|
t.Fatal("non-nil route registrar was not called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue