优化结构

This commit is contained in:
Yvan 2026-08-25 00:20:29 +08:00
parent 969afee284
commit ee179d6b34
49 changed files with 289 additions and 591 deletions

View File

@ -158,14 +158,6 @@ func (uc *SecurityUsecase) ValidatePassword(value *SecurityConfig, password stri
return nil
}
func (uc *SecurityUsecase) ValidateCurrentPassword(ctx context.Context, password string) error {
config, err := uc.Current(ctx)
if err != nil {
return err
}
return uc.ValidatePassword(config, password)
}
func (uc *SecurityUsecase) LoginLocked(ctx context.Context, username string) (bool, error) {
_, locked, err := uc.cache.Get(ctx, loginLockKey(username))
return locked, err

View File

@ -69,16 +69,6 @@ func (uc *UserUsecase) Login(ctx context.Context, username, password string) (*U
return u, nil
}
func (uc *UserUsecase) User(ctx context.Context, id uint) (*User, error) {
user, err := uc.repo.FindUserByID(ctx, id)
if err != nil {
return nil, err
}
_ = uc.repo.FillDepartmentNamePaths(ctx, user)
uc.fallbackDefaultRouter(ctx, user)
return user, nil
}
// UserByUUID resolves the identity from the UUID embedded in the JWT rather
// than from the mutable numeric ID used by administrative records.
func (uc *UserUsecase) UserByUUID(ctx context.Context, uuid string) (*User, error) {

View File

@ -0,0 +1,35 @@
package config
// MaskedSecret is the placeholder written in place of a stored secret whenever a
// configuration payload leaves the process. Every layer that masks or preserves
// configuration secrets shares this sentinel so a masked read round-trips into a
// write without silently overwriting the stored value.
const MaskedSecret = "******"
// IsMaskedSecret reports whether an inbound value carries no new secret, either
// because it was omitted or because it is the mask handed out on read.
func IsMaskedSecret(value string) bool { return value == "" || value == MaskedSecret }
// ObjectStores lists every object-store block of a storage configuration so
// callers iterate the providers instead of repeating the list.
func ObjectStores(value *Storage) []*ObjectStore {
if value == nil {
return nil
}
return []*ObjectStore{value.AliyunOSS, value.HuaweiOBS, value.TencentCOS, value.AWSS3, value.CloudflareR2, value.Minio}
}
// MaskStorageSecrets replaces every populated storage secret with MaskedSecret.
func MaskStorageSecrets(value *Storage) {
if value == nil {
return
}
if value.Qiniu != nil && value.Qiniu.SecretKey != "" {
value.Qiniu.SecretKey = MaskedSecret
}
for _, item := range ObjectStores(value) {
if item != nil && item.SecretKey != "" {
item.SecretKey = MaskedSecret
}
}
}

View File

@ -10,24 +10,3 @@ func cloneAdminConfig(value *config.Admin) *config.Admin {
}
return config.CloneAdmin(value)
}
func maskStorageSecrets(storage *config.Storage) {
if storage == nil {
return
}
if storage.Qiniu != nil && storage.Qiniu.SecretKey != "" {
storage.Qiniu.SecretKey = "******"
}
for _, item := range []*config.ObjectStore{
storage.AliyunOSS,
storage.HuaweiOBS,
storage.TencentCOS,
storage.AWSS3,
storage.CloudflareR2,
storage.Minio,
} {
if item != nil && item.SecretKey != "" {
item.SecretKey = "******"
}
}
}

View File

@ -8,6 +8,7 @@ import (
"strings"
"time"
"kra/internal/config"
"kra/internal/integration/runtimeconfig"
"gorm.io/gorm"
@ -158,7 +159,7 @@ func maskIntegrationSecrets(kind, provider string, values map[string]any) {
for key, value := range values {
if secretFields[key] || likelyIntegrationSecret(key) {
if text, ok := value.(string); ok && text != "" {
values[key] = "******"
values[key] = config.MaskedSecret
}
continue
}
@ -172,7 +173,7 @@ func mergeIntegrationSecrets(kind, provider string, values, old map[string]any)
secretFields := integrationSecretFields(kind, provider)
for key, value := range values {
if secretFields[key] || likelyIntegrationSecret(key) {
if text, ok := value.(string); ok && text == "******" {
if text, ok := value.(string); ok && text == config.MaskedSecret {
if prior, exists := old[key]; exists {
values[key] = prior
}

View File

@ -157,11 +157,11 @@ func TestMaskStorageSecretsLeavesUnconfiguredProvidersEmpty(t *testing.T) {
AliyunOSS: &config.ObjectStore{SecretKey: "configured-secret"},
Minio: &config.ObjectStore{},
}
maskStorageSecrets(storage)
config.MaskStorageSecrets(storage)
if storage.Qiniu.SecretKey != "" || storage.Minio.SecretKey != "" {
t.Fatalf("empty provider secrets were masked: %#v", storage)
}
if storage.AliyunOSS.SecretKey != "******" {
if storage.AliyunOSS.SecretKey != config.MaskedSecret {
t.Fatalf("configured secret was not masked: %q", storage.AliyunOSS.SecretKey)
}
}

View File

@ -77,93 +77,6 @@ func (r *ResourceRegistry) Replace(resources Resources) {
r.mu.Unlock()
}
func (r *ResourceRegistry) Logger() *slog.Logger { return r.Snapshot().Logger }
func (r *ResourceRegistry) SetLogger(logger *slog.Logger) {
r.update(func(resources *Resources) { resources.Logger = logger })
}
func (r *ResourceRegistry) DB() *gorm.DB { return r.Snapshot().DB }
func (r *ResourceRegistry) SetDB(db *gorm.DB) {
r.update(func(resources *Resources) { resources.DB = db })
}
// NamedDB returns a configured secondary database by alias. An empty alias
// resolves to the primary database.
func (r *ResourceRegistry) NamedDB(name string) *gorm.DB {
if name == "" {
return r.DB()
}
return r.Snapshot().NamedDBs[name]
}
// SetNamedDBs replaces the complete secondary-database alias set.
func (r *ResourceRegistry) SetNamedDBs(databases map[string]*gorm.DB) {
r.update(func(resources *Resources) { resources.NamedDBs = cloneDBMap(databases) })
}
func (r *ResourceRegistry) Redis() redis.UniversalClient { return r.Snapshot().Redis }
func (r *ResourceRegistry) SetRedis(client redis.UniversalClient) {
r.update(func(resources *Resources) { resources.Redis = client })
}
// NamedRedis returns a configured secondary Redis client by alias. An empty
// alias resolves to the primary Redis client.
func (r *ResourceRegistry) NamedRedis(name string) redis.UniversalClient {
if name == "" {
return r.Redis()
}
return r.Snapshot().NamedRedis[name]
}
// SetNamedRedis replaces the complete secondary-Redis alias set.
func (r *ResourceRegistry) SetNamedRedis(clients map[string]redis.UniversalClient) {
r.update(func(resources *Resources) { resources.NamedRedis = cloneRedisMap(clients) })
}
func (r *ResourceRegistry) Mongo() *mongo.Client { return r.Snapshot().Mongo }
func (r *ResourceRegistry) SetMongo(client *mongo.Client) {
r.update(func(resources *Resources) { resources.Mongo = client })
}
func (r *ResourceRegistry) Storage() FileStorage { return r.Snapshot().Storage }
func (r *ResourceRegistry) SetStorage(storage FileStorage) {
r.update(func(resources *Resources) { resources.Storage = storage })
}
func (r *ResourceRegistry) MQ() platformmq.Registry { return r.Snapshot().MQ }
func (r *ResourceRegistry) SetMQ(registry platformmq.Registry) {
r.update(func(resources *Resources) { resources.MQ = registry })
}
func (r *ResourceRegistry) WebSocket() platformws.Hub { return r.Snapshot().WebSocket }
func (r *ResourceRegistry) SetWebSocket(hub platformws.Hub) {
r.update(func(resources *Resources) { resources.WebSocket = hub })
}
func (r *ResourceRegistry) Scheduler() Scheduler { return r.Snapshot().Scheduler }
func (r *ResourceRegistry) SetScheduler(scheduler Scheduler) {
r.update(func(resources *Resources) { resources.Scheduler = scheduler })
}
func (r *ResourceRegistry) update(change func(*Resources)) {
if r == nil || change == nil {
return
}
r.mu.Lock()
resources := cloneResources(r.resources)
change(&resources)
r.resources = normalizeResources(resources)
r.mu.Unlock()
}
func normalizeResources(resources Resources) Resources {
if resources.Logger == nil {
resources.Logger = slog.Default()
@ -248,45 +161,3 @@ func Install(resources Resources) *ResourceRegistry {
func DefaultRegistry() *ResourceRegistry { return defaultResources }
func ResourceSnapshot() Resources { return defaultResources.Snapshot() }
func ReplaceResources(resources Resources) { defaultResources.Replace(resources) }
func Logger() *slog.Logger { return defaultResources.Logger() }
func SetLogger(logger *slog.Logger) { defaultResources.SetLogger(logger) }
func DB() *gorm.DB { return defaultResources.DB() }
func SetDB(db *gorm.DB) { defaultResources.SetDB(db) }
func NamedDB(name string) *gorm.DB { return defaultResources.NamedDB(name) }
func SetNamedDBs(databases map[string]*gorm.DB) { defaultResources.SetNamedDBs(databases) }
func Redis() redis.UniversalClient { return defaultResources.Redis() }
func SetRedis(client redis.UniversalClient) { defaultResources.SetRedis(client) }
func NamedRedis(name string) redis.UniversalClient { return defaultResources.NamedRedis(name) }
func SetNamedRedis(clients map[string]redis.UniversalClient) { defaultResources.SetNamedRedis(clients) }
func Mongo() *mongo.Client { return defaultResources.Mongo() }
func SetMongo(client *mongo.Client) { defaultResources.SetMongo(client) }
func Storage() FileStorage { return defaultResources.Storage() }
func SetStorage(storage FileStorage) { defaultResources.SetStorage(storage) }
func MQ() platformmq.Registry { return defaultResources.MQ() }
func SetMQ(registry platformmq.Registry) { defaultResources.SetMQ(registry) }
func WebSocket() platformws.Hub { return defaultResources.WebSocket() }
func SetWebSocket(hub platformws.Hub) { defaultResources.SetWebSocket(hub) }
func TaskScheduler() Scheduler { return defaultResources.Scheduler() }
func SetTaskScheduler(scheduler Scheduler) { defaultResources.SetScheduler(scheduler) }

View File

@ -55,16 +55,16 @@ func (*webSocketStub) OnDisconnect(func(*platformws.Session)) {}
func TestResourceRegistryDefaults(t *testing.T) {
registry := NewResourceRegistry()
if registry.Logger() == nil {
resources := registry.Snapshot()
if resources.Logger == nil {
t.Fatal("default logger is nil")
}
resources := registry.Snapshot()
if resources.DB != nil || len(resources.NamedDBs) != 0 || resources.Redis != nil || len(resources.NamedRedis) != 0 || resources.Mongo != nil || resources.Storage != nil || resources.MQ != nil || resources.WebSocket != nil || resources.Scheduler != nil {
t.Fatalf("unexpected initialized resource: %#v", resources)
}
}
func TestResourceRegistryReplaceAndSetters(t *testing.T) {
func TestResourceRegistryReplaceAndSnapshot(t *testing.T) {
logger := slog.Default().With("test", true)
db := &gorm.DB{}
redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
@ -76,41 +76,33 @@ func TestResourceRegistryReplaceAndSetters(t *testing.T) {
scheduler := &schedulerStub{}
registry := NewResourceRegistry()
inputDBs := map[string]*gorm.DB{"reporting": db}
inputRedis := map[string]redis.UniversalClient{"cache": redisClient}
registry.Replace(Resources{
Logger: logger, DB: db, NamedDBs: map[string]*gorm.DB{"reporting": db},
Redis: redisClient, NamedRedis: map[string]redis.UniversalClient{"cache": redisClient}, Mongo: mongoClient,
Logger: logger, DB: db, NamedDBs: inputDBs,
Redis: redisClient, NamedRedis: inputRedis, Mongo: mongoClient,
Storage: storage, MQ: mq, WebSocket: webSocket, Scheduler: scheduler,
})
resources := registry.Snapshot()
if resources.Logger != logger || resources.DB != db || resources.Redis != redisClient || resources.Mongo != mongoClient || resources.Storage != storage || resources.MQ != mq || resources.WebSocket != webSocket || resources.Scheduler != scheduler {
t.Fatalf("replacement snapshot does not match: %#v", resources)
}
if resources.NamedDBs["reporting"] != db || resources.NamedRedis["cache"] != redisClient {
t.Fatal("named resource lookup returned the wrong resource")
}
nextDB := &gorm.DB{}
registry.SetDB(nextDB)
if registry.DB() != nextDB {
t.Fatal("database setter did not publish the new database")
}
if registry.Redis() != redisClient || registry.Storage() != storage {
t.Fatal("database setter changed unrelated resources")
}
if registry.NamedDB("reporting") != db || registry.NamedDB("") != nextDB {
t.Fatal("named database lookup returned the wrong resource")
}
if registry.NamedRedis("cache") != redisClient || registry.NamedRedis("") != redisClient {
t.Fatal("named redis lookup returned the wrong resource")
}
inputDBs := map[string]*gorm.DB{"analytics": db}
registry.SetNamedDBs(inputDBs)
inputDBs["mutated"] = nextDB
if registry.NamedDB("mutated") != nil || registry.NamedDB("analytics") != db {
t.Fatal("named database map was not copied")
}
inputRedis := map[string]redis.UniversalClient{"sessions": redisClient}
registry.SetNamedRedis(inputRedis)
inputDBs["mutated"] = &gorm.DB{}
inputRedis["mutated"] = redisClient
if registry.NamedRedis("mutated") != nil || registry.NamedRedis("sessions") != redisClient {
t.Fatal("named redis map was not copied")
if _, exists := registry.Snapshot().NamedDBs["mutated"]; exists {
t.Fatal("named database map was not copied on replacement")
}
if _, exists := registry.Snapshot().NamedRedis["mutated"]; exists {
t.Fatal("named redis map was not copied on replacement")
}
resources.NamedDBs["mutated"] = db
if _, exists := registry.Snapshot().NamedDBs["mutated"]; exists {
t.Fatal("snapshot shares its named database map with the registry")
}
}
@ -140,14 +132,16 @@ func TestResourceRegistryConcurrentAccess(t *testing.T) {
go func() {
defer wait.Done()
for step := 0; step < iterations; step++ {
registry.SetDB(&gorm.DB{})
_ = registry.Snapshot()
_ = registry.Logger()
registry.Replace(Resources{DB: &gorm.DB{}})
if registry.Snapshot().Logger == nil {
t.Error("concurrent snapshot lost the default logger")
return
}
}
}()
}
wait.Wait()
if registry.DB() == nil {
t.Fatal("concurrent setters lost the database")
if registry.Snapshot().DB == nil {
t.Fatal("concurrent replacement lost the database")
}
}

View File

@ -322,12 +322,12 @@ func maskConfigSecrets(value *config.Config) {
return
}
if value.Admin.JWT != nil && value.Admin.JWT.SigningKey != "" {
value.Admin.JWT.SigningKey = "******"
value.Admin.JWT.SigningKey = config.MaskedSecret
}
if value.Admin.Email != nil && value.Admin.Email.Secret != "" {
value.Admin.Email.Secret = "******"
value.Admin.Email.Secret = config.MaskedSecret
}
maskStorageSecrets(value.Admin.Storage)
config.MaskStorageSecrets(value.Admin.Storage)
}
func maskDataSecrets(value *config.Data) {
@ -335,24 +335,24 @@ func maskDataSecrets(value *config.Data) {
return
}
if value.Database != nil {
value.Database.Password = "******"
value.Database.Password = config.MaskedSecret
value.Database.Source = ""
}
if value.Redis != nil {
value.Redis.Password = "******"
value.Redis.Password = config.MaskedSecret
}
if value.Mongo != nil {
value.Mongo.Password = "******"
value.Mongo.Password = config.MaskedSecret
}
for _, item := range value.DatabaseList {
if item != nil {
item.Password = "******"
item.Password = config.MaskedSecret
item.Source = ""
}
}
for _, item := range value.RedisList {
if item != nil {
item.Password = "******"
item.Password = config.MaskedSecret
}
}
}
@ -365,10 +365,10 @@ func preserveConfigSecrets(next, current *config.Config) {
if next.Admin == nil || current.Admin == nil {
return
}
if next.Admin.JWT != nil && current.Admin.JWT != nil && maskedSecret(next.Admin.JWT.SigningKey) {
if next.Admin.JWT != nil && current.Admin.JWT != nil && config.IsMaskedSecret(next.Admin.JWT.SigningKey) {
next.Admin.JWT.SigningKey = current.Admin.JWT.SigningKey
}
if next.Admin.Email != nil && current.Admin.Email != nil && maskedSecret(next.Admin.Email.Secret) {
if next.Admin.Email != nil && current.Admin.Email != nil && config.IsMaskedSecret(next.Admin.Email.Secret) {
next.Admin.Email.Secret = current.Admin.Email.Secret
}
preserveStorageSecrets(next.Admin.Storage, current.Admin.Storage)
@ -379,17 +379,17 @@ func preserveDataSecrets(next, current *config.Data) {
return
}
if next.Database != nil && current.Database != nil {
if maskedSecret(next.Database.Password) {
if config.IsMaskedSecret(next.Database.Password) {
next.Database.Password = current.Database.Password
}
if next.Database.Source == "" {
next.Database.Source = current.Database.Source
}
}
if next.Redis != nil && current.Redis != nil && maskedSecret(next.Redis.Password) {
if next.Redis != nil && current.Redis != nil && config.IsMaskedSecret(next.Redis.Password) {
next.Redis.Password = current.Redis.Password
}
if next.Mongo != nil && current.Mongo != nil && maskedSecret(next.Mongo.Password) {
if next.Mongo != nil && current.Mongo != nil && config.IsMaskedSecret(next.Mongo.Password) {
next.Mongo.Password = current.Mongo.Password
}
preserveDatabaseListSecrets(next.DatabaseList, current.DatabaseList)
@ -414,7 +414,7 @@ func preserveDatabaseListSecrets(next, current []*config.Database) {
if previous == nil {
continue
}
if maskedSecret(item.Password) {
if config.IsMaskedSecret(item.Password) {
item.Password = previous.Password
}
if item.Source == "" {
@ -438,43 +438,22 @@ func preserveRedisListSecrets(next, current []*config.Redis) {
if previous == nil && index < len(current) {
previous = current[index]
}
if previous != nil && maskedSecret(item.Password) {
if previous != nil && config.IsMaskedSecret(item.Password) {
item.Password = previous.Password
}
}
}
func maskedSecret(value string) bool { return value == "" || value == "******" }
func objectStores(value *config.Storage) []*config.ObjectStore {
if value == nil {
return nil
}
return []*config.ObjectStore{value.AliyunOSS, value.HuaweiOBS, value.TencentCOS, value.AWSS3, value.CloudflareR2, value.Minio}
}
func maskStorageSecrets(value *config.Storage) {
if value == nil {
return
}
if value.Qiniu != nil && value.Qiniu.SecretKey != "" {
value.Qiniu.SecretKey = "******"
}
for _, item := range objectStores(value) {
if item != nil && item.SecretKey != "" {
item.SecretKey = "******"
}
}
}
func preserveStorageSecrets(next, current *config.Storage) {
if next == nil || current == nil {
return
}
if next.Qiniu != nil && current.Qiniu != nil && maskedSecret(next.Qiniu.SecretKey) {
if next.Qiniu != nil && current.Qiniu != nil && config.IsMaskedSecret(next.Qiniu.SecretKey) {
next.Qiniu.SecretKey = current.Qiniu.SecretKey
}
nextItems, currentItems := objectStores(next), objectStores(current)
nextItems, currentItems := config.ObjectStores(next), config.ObjectStores(current)
for index := range nextItems {
if nextItems[index] != nil && currentItems[index] != nil && maskedSecret(nextItems[index].SecretKey) {
if nextItems[index] != nil && currentItems[index] != nil && config.IsMaskedSecret(nextItems[index].SecretKey) {
nextItems[index].SecretKey = currentItems[index].SecretKey
}
}

View File

@ -8,6 +8,7 @@ import (
integrationbiz "kra/internal/biz/integration"
"strings"
"kra/internal/config"
"kra/internal/integration/mq"
"kra/internal/integration/runtimeconfig"
websocketintegration "kra/internal/integration/websocket"
@ -73,11 +74,11 @@ func (t *ConnectivityTester) restoreMaskedSecrets(kind, provider string, values
}
for key := range masked {
value, _ := values[key].(string)
if strings.TrimSpace(value) != "******" {
if strings.TrimSpace(value) != config.MaskedSecret {
continue
}
prior, _ := currentValues[key].(string)
if strings.TrimSpace(prior) == "" || strings.TrimSpace(prior) == "******" {
if config.IsMaskedSecret(strings.TrimSpace(prior)) {
return fmt.Errorf("配置字段 %s 已脱敏,请重新填写后再测试", key)
}
values[key] = prior

View File

@ -14,17 +14,13 @@ func NewAnnouncement(service *service.AnnouncementService) *Announcement {
return &Announcement{service: service}
}
func announcementInput(req dto.AnnouncementRequest) service.AnnouncementInput {
return service.AnnouncementInput{ID: req.ID, Title: req.Title, Content: req.Content, UserID: req.UserID, Attachments: req.Attachments}
}
func (h *Announcement) Create(c *gin.Context) {
var req dto.AnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
Fail(c, err.Error())
return
}
if err := h.service.Create(c.Request.Context(), announcementInput(req)); err != nil {
if err := h.service.Create(c.Request.Context(), &req); err != nil {
Fail(c, "创建失败")
return
}
@ -53,7 +49,7 @@ func (h *Announcement) Update(c *gin.Context) {
Fail(c, err.Error())
return
}
if err := h.service.Update(c.Request.Context(), announcementInput(req)); err != nil {
if err := h.service.Update(c.Request.Context(), &req); err != nil {
Fail(c, "更新失败")
return
}

View File

@ -3,6 +3,7 @@ package middleware
import (
"context"
"kra/internal/biz/system"
httpx "kra/pkg/httpx"
"kra/internal/config"
"kra/internal/utils/routepath"
@ -19,11 +20,11 @@ func AccessControl(runtime *config.Store, access accessController) gin.HandlerFu
return func(c *gin.Context) {
claims := Claims(c)
if claims == nil {
NoAuth(c, "未登录或非法访问")
httpx.NoAuth(c, "未登录或非法访问")
return
}
if access == nil {
Write(c, CodeError, gin.H{}, "权限服务不可用")
httpx.Write(c, httpx.CodeError, gin.H{}, "权限服务不可用")
c.Abort()
return
}
@ -36,13 +37,13 @@ func AccessControl(runtime *config.Store, access accessController) gin.HandlerFu
}
allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, policyPath, c.Request.Method)
if err != nil || !allowed {
Write(c, CodeError, gin.H{}, "权限不足")
httpx.Write(c, httpx.CodeError, gin.H{}, "权限不足")
c.Abort()
return
}
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
if err != nil {
Write(c, CodeError, gin.H{}, "数据权限解析失败")
httpx.Write(c, httpx.CodeError, gin.H{}, "数据权限解析失败")
c.Abort()
return
}

View File

@ -7,6 +7,7 @@ import (
"errors"
"io"
"kra/internal/biz/system"
httpx "kra/pkg/httpx"
"log/slog"
"mime"
"net/http"
@ -51,7 +52,7 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
c.Header("X-Kra-Version", version)
requestReadFailed := c.Request.ContentLength > bodyLimit
if requestReadFailed {
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": CodeError, "msg": "请求体超过大小上限"})
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
} else if c.Request.Body != nil && !mediaUpload {
limited := http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
var err error
@ -60,9 +61,9 @@ func AccessLog(runtime *config.Store, logger *slog.Logger, version string) gin.H
requestReadFailed = true
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": CodeError, "msg": "请求体超过大小上限"})
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": CodeError, "msg": "请求体读取失败"})
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": httpx.CodeError, "msg": "请求体读取失败"})
}
} else {
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
@ -204,9 +205,8 @@ func paymentConfigSummary(body []byte) string {
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 lower == "authorization" || lower == "cookie" || lower == "set-cookie" || lower == "x-token" {
out[key] = "***"
if isSensitiveHeader(key) {
out[key] = redactedValue
} else {
out[key] = strings.Join(values, ",")
}
@ -220,23 +220,13 @@ func redactQuery(raw string) string {
}
values, _ := url.ParseQuery(raw)
for key := range values {
if sensitiveQueryKey(key) {
values[key] = []string{"***"}
if isSensitivePayloadKey(key) {
values[key] = []string{redactedValue}
}
}
return values.Encode()
}
func sensitiveQueryKey(key string) bool {
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
switch normalized {
case "token", "accesstoken", "refreshtoken", "authorization", "apikey", "secret":
return true
default:
return strings.HasSuffix(normalized, "token")
}
}
func stringValueFromContext(c *gin.Context, key string) string {
value, _ := c.Get(key)
return stringValue(value)

View File

@ -135,25 +135,12 @@ func operationRequestBody(raw []byte, contentType string, limit int) string {
return text
}
// operationSecretKeys holds the normalized JSON keys whose values never reach
// the operation record. Keys are compared after lowercasing and stripping
// separators, so "new_password" and "newPassword" both match "newpassword".
var operationSecretKeys = map[string]struct{}{
"password": {}, "newpassword": {}, "oldpassword": {}, "confirmpassword": {},
"passwd": {}, "pwd": {}, "token": {}, "accesstoken": {}, "refreshtoken": {},
"secret": {}, "clientsecret": {}, "apikey": {}, "privatekey": {}, "idcard": {},
"appkey": {}, "mchkey": {}, "apiv3key": {}, "clientcert": {}, "clientkey": {},
"platformcert": {}, "platformserialno": {}, "credentialcode": {}, "certfile": {},
"keyfile": {}, "publickey": {}, "rootcert": {}, "appcert": {}, "webhookid": {},
}
func maskOperationBody(value any) {
switch current := value.(type) {
case map[string]any:
for key, item := range current {
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
if _, secret := operationSecretKeys[normalized]; secret {
current[key] = "***"
if isSensitivePayloadKey(key) {
current[key] = redactedValue
continue
}
maskOperationBody(item)

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"kra/internal/biz/system"
httpx "kra/pkg/httpx"
"net/http"
"strconv"
"strings"
@ -43,11 +44,11 @@ func AuthenticateWebSocket(c *gin.Context, auth TokenAuthenticator) bool {
func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool) bool {
token := RequestToken(c, allowQueryToken)
if token == "" {
NoAuth(c, "未登录或非法访问,请登录")
httpx.NoAuth(c, "未登录或非法访问,请登录")
return false
}
if auth == nil {
NoAuth(c, "认证服务不可用")
httpx.NoAuth(c, "认证服务不可用")
return false
}
// Followers of a singleflight flight must not inherit the leader's request
@ -61,20 +62,20 @@ func authenticate(c *gin.Context, auth TokenAuthenticator, allowQueryToken bool)
return auth.AuthenticateToken(ctx, token)
})
if err != nil {
SetTokenCookie(c, "", -1)
NoAuth(c, tokenErrorMessage(err))
httpx.SetTokenCookie(c, "", -1)
httpx.NoAuth(c, tokenErrorMessage(err))
return false
}
authentication, ok := value.(*system.TokenAuthentication)
if !ok || authentication == nil || authentication.Claims == nil {
SetTokenCookie(c, "", -1)
NoAuth(c, "无法处理此token")
httpx.SetTokenCookie(c, "", -1)
httpx.NoAuth(c, "无法处理此token")
return false
}
if authentication.Refreshed != nil {
c.Header("new-token", authentication.Refreshed.Value)
c.Header("new-expires-at", strconv.FormatInt(authentication.Refreshed.ExpiresAt.Unix(), 10))
SetTokenCookie(c, authentication.Refreshed.Value, int(authentication.Refreshed.TTL.Seconds()))
httpx.SetTokenCookie(c, authentication.Refreshed.Value, int(authentication.Refreshed.TTL.Seconds()))
}
c.Set(claimsKey, authentication.Claims)
return true
@ -140,6 +141,6 @@ func MustChangePassword() gin.HandlerFunc {
c.Next()
return
}
c.AbortWithStatusJSON(http.StatusConflict, Response{Code: CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
c.AbortWithStatusJSON(http.StatusConflict, httpx.Response{Code: httpx.CodePasswordChangeRequired, Data: gin.H{"needChangePassword": true}, Msg: "密码已过期,请先修改密码"})
}
}

View File

@ -3,6 +3,7 @@ package middleware
import (
"bytes"
"encoding/json"
httpx "kra/pkg/httpx"
"log/slog"
"strings"
@ -24,7 +25,7 @@ func ErrorAudit(logger *slog.Logger) gin.HandlerFunc {
if auditPersistFailed != true && (strings.Contains(c.Request.URL.Path, "/sysError/") || strings.Contains(c.Request.URL.Path, "/logViewer/") || c.Writer.Status() >= 300 && c.Writer.Status() < 500) {
return
}
var response Response
var response httpx.Response
var body []byte
if value, ok := c.Get(ctxRespBufferKey); ok {
if buffer, valid := value.(*bytes.Buffer); valid {
@ -34,7 +35,7 @@ func ErrorAudit(logger *slog.Logger) gin.HandlerFunc {
if json.Unmarshal(body, &response) != nil && privateErrors == "" {
return
}
if response.Code == CodeSuccess && privateErrors == "" {
if response.Code == httpx.CodeSuccess && privateErrors == "" {
return
}
if privateErrors == "" && expectedClientFailure(response.Msg) {

View File

@ -1,18 +0,0 @@
package middleware
import (
"github.com/gin-gonic/gin"
httpx "kra/pkg/httpx"
)
const (
CodeSuccess = httpx.CodeSuccess
CodeError = httpx.CodeError
CodePasswordChangeRequired = httpx.CodePasswordChangeRequired
)
type Response = httpx.Response
func Write(c *gin.Context, code int, data any, message string) { httpx.Write(c, code, data, message) }
func NoAuth(c *gin.Context, message string) { httpx.NoAuth(c, message) }
func SetTokenCookie(c *gin.Context, value string, maxAge int) { httpx.SetTokenCookie(c, value, maxAge) }

View File

@ -2,6 +2,7 @@ package middleware
import (
"kra/internal/service"
httpx "kra/pkg/httpx"
"net/http"
"strings"
"time"
@ -22,7 +23,7 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
}
config, err := settings.CurrentSecurity(c.Request.Context())
if err != nil || config == nil {
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
return
}
if !config.LimitEnable {
@ -40,11 +41,11 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
key := "KRA_SecLimit:" + c.ClientIP() + ":" + route
count, cacheErr := settings.IncrementRateLimit(c.Request.Context(), key, time.Duration(window)*time.Second)
if cacheErr != nil {
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": CodeError, "msg": "安全服务暂不可用"})
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
return
}
if int(count) > config.LimitCount {
c.JSON(200, gin.H{"code": CodeError, "msg": "请求太过频繁,请稍后再试"})
c.JSON(200, gin.H{"code": httpx.CodeError, "msg": "请求太过频繁,请稍后再试"})
c.Abort()
return
}

View File

@ -10,7 +10,7 @@ import (
"testing"
"time"
"kra/internal/service"
systemservice "kra/internal/service/system"
"github.com/gin-gonic/gin"
)
@ -38,7 +38,7 @@ func (c rateLimitCache) Increment(context.Context, string, time.Duration) (int64
func TestSecurityRateLimitMatchesResponseContract(t *testing.T) {
gin.SetMode(gin.TestMode)
settings := service.NewSecurityService(system.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{}, nil, nil))
settings := systemservice.NewSecurityService(system.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{}, nil, nil))
engine := gin.New()
engine.Use(SecurityRateLimit(settings))
engine.POST("/base/login", func(c *gin.Context) { c.Status(http.StatusNoContent) })
@ -56,7 +56,7 @@ func TestSecurityRateLimitMatchesResponseContract(t *testing.T) {
func TestSecurityRateLimitFailsClosedWhenCacheIsUnavailable(t *testing.T) {
gin.SetMode(gin.TestMode)
settings := service.NewSecurityService(system.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{err: errors.New("cache unavailable")}, nil, nil))
settings := systemservice.NewSecurityService(system.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{err: errors.New("cache unavailable")}, nil, nil))
engine := gin.New()
called := false
engine.Use(SecurityRateLimit(settings))

View File

@ -64,9 +64,8 @@ func recoveryRequestDump(request *http.Request) string {
clone := request.Clone(request.Context())
clone.Header = request.Header.Clone()
for key := range clone.Header {
switch strings.ToLower(key) {
case "authorization", "cookie", "set-cookie", "x-token", "proxy-authorization":
clone.Header[key] = []string{"***"}
if isSensitiveHeader(key) {
clone.Header[key] = []string{redactedValue}
}
}
if request.URL != nil {

View File

@ -0,0 +1,41 @@
package middleware
import "strings"
// redactedValue replaces a sensitive value in every log line and audit record.
const redactedValue = "***"
// sensitiveHeaders lists the headers whose value never reaches a log, compared
// after lowercasing.
var sensitiveHeaders = map[string]struct{}{
"authorization": {}, "proxy-authorization": {}, "cookie": {}, "set-cookie": {}, "x-token": {},
}
// sensitivePayloadKeys holds the normalized query and JSON body keys whose
// values never reach a log or an operation record. Keys are compared after
// lowercasing and stripping separators, so "new_password" and "newPassword"
// both match "newpassword".
var sensitivePayloadKeys = map[string]struct{}{
"password": {}, "newpassword": {}, "oldpassword": {}, "confirmpassword": {},
"passwd": {}, "pwd": {}, "token": {}, "accesstoken": {}, "refreshtoken": {},
"secret": {}, "clientsecret": {}, "apikey": {}, "privatekey": {}, "idcard": {},
"appkey": {}, "mchkey": {}, "apiv3key": {}, "clientcert": {}, "clientkey": {},
"platformcert": {}, "platformserialno": {}, "credentialcode": {}, "certfile": {},
"keyfile": {}, "publickey": {}, "rootcert": {}, "appcert": {}, "webhookid": {},
"authorization": {},
}
func isSensitiveHeader(key string) bool {
_, sensitive := sensitiveHeaders[strings.ToLower(key)]
return sensitive
}
// isSensitivePayloadKey also matches every key ending in "token" so provider
// specific token names stay masked without being enumerated.
func isSensitivePayloadKey(key string) bool {
normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(key, "_", ""), "-", ""))
if _, sensitive := sensitivePayloadKeys[normalized]; sensitive {
return true
}
return strings.HasSuffix(normalized, "token")
}

View File

@ -1,27 +0,0 @@
package router
import "kra/internal/server/handler"
type Set = handler.Set
type Announcement = handler.Announcement
type API = handler.API
type APIToken = handler.APIToken
type Audit = handler.Audit
type Authority = handler.Authority
type Dictionary = handler.Dictionary
type Email = handler.Email
type Export = handler.Export
type Media = handler.Media
type Menu = handler.Menu
type Navigation = handler.Navigation
type Organization = handler.Organization
type Parameter = handler.Parameter
type Payment = handler.Payment
type Permission = handler.Permission
type Public = handler.Public
type Session = handler.Session
type SystemConfig = handler.SystemConfig
type Task = handler.Task
type User = handler.User
type Version = handler.Version
type IntegrationConfig = handler.IntegrationConfig

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterAnnouncement(private, public *gin.RouterGroup, h *Announcement) {
func RegisterAnnouncement(private, public *gin.RouterGroup, h *handler.Announcement) {
privateInfo := private.Group("/info")
privateInfo.POST("/createInfo", h.Create)
privateInfo.DELETE("/deleteInfo", h.Delete)

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterAPI(group, public *gin.RouterGroup, engine *gin.Engine, h *API) {
func RegisterAPI(group, public *gin.RouterGroup, engine *gin.Engine, h *handler.API) {
api := group.Group("/api")
api.POST("/getApiList", h.List)
api.POST("/getAllApis", h.All)

View File

@ -2,11 +2,12 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterAPIToken(group *gin.RouterGroup, handler *APIToken) {
func RegisterAPIToken(group *gin.RouterGroup, h *handler.APIToken) {
router := group.Group("/sysApiToken")
router.POST("/createApiToken", handler.Create)
router.POST("/getApiTokenList", handler.List)
router.POST("/deleteApiToken", handler.Delete)
router.POST("/createApiToken", h.Create)
router.POST("/getApiTokenList", h.List)
router.POST("/deleteApiToken", h.Delete)
}

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterAudit(group, public *gin.RouterGroup, h *Audit) {
func RegisterAudit(group, public *gin.RouterGroup, h *handler.Audit) {
operations := group.Group("/sysOperationRecord")
operations.GET("/getSysOperationRecordList", h.Operations)
operations.GET("/findSysOperationRecord", h.Operation)

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterAuthority(group *gin.RouterGroup, h *Authority) {
func RegisterAuthority(group *gin.RouterGroup, h *handler.Authority) {
router := group.Group("/authority")
router.POST("/getAuthorityList", h.List)
router.POST("/createAuthority", h.Create)

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterDictionary(group *gin.RouterGroup, h *Dictionary) {
func RegisterDictionary(group *gin.RouterGroup, h *handler.Dictionary) {
dictionaries := group.Group("/sysDictionary")
dictionaries.POST("/createSysDictionary", h.Create)
dictionaries.PUT("/updateSysDictionary", h.Update)

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterEmail(group *gin.RouterGroup, h *Email) {
func RegisterEmail(group *gin.RouterGroup, h *handler.Email) {
email := group.Group("/email")
email.POST("/emailTest", h.Test)
email.POST("/sendEmail", h.Send)

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterExport(group, public *gin.RouterGroup, h *Export) {
func RegisterExport(group, public *gin.RouterGroup, h *handler.Export) {
router := group.Group("/sysExportTemplate")
router.POST("/createSysExportTemplate", h.Create)
router.PUT("/updateSysExportTemplate", h.Update)

View File

@ -1,12 +1,16 @@
package router
import "github.com/gin-gonic/gin"
import (
"kra/internal/server/handler"
func RegisterIntegrationConfig(group *gin.RouterGroup, handler *IntegrationConfig) {
"github.com/gin-gonic/gin"
)
func RegisterIntegrationConfig(group *gin.RouterGroup, h *handler.IntegrationConfig) {
configs := group.Group("/integration/configs")
configs.GET("/:kind", handler.List)
configs.GET("/:kind/:provider", handler.Find)
configs.PUT("/:kind/:provider", handler.Save)
configs.POST("/:kind/:provider/test", handler.Test)
configs.DELETE("/:kind/:provider", handler.Delete)
configs.GET("/:kind", h.List)
configs.GET("/:kind/:provider", h.Find)
configs.PUT("/:kind/:provider", h.Save)
configs.POST("/:kind/:provider/test", h.Test)
configs.DELETE("/:kind/:provider", h.Delete)
}

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterMedia(group *gin.RouterGroup, h *Media) {
func RegisterMedia(group *gin.RouterGroup, h *handler.Media) {
files := group.Group("/fileUploadAndDownload")
files.POST("/upload", h.Upload)
files.POST("/getFileList", h.List)

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterMenu(group *gin.RouterGroup, h *Menu) {
func RegisterMenu(group *gin.RouterGroup, h *handler.Menu) {
router := group.Group("/menu")
router.POST("/getMenuList", h.List)
router.POST("/getBaseMenuTree", h.Tree)

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterOrganization(group *gin.RouterGroup, h *Organization) {
func RegisterOrganization(group *gin.RouterGroup, h *handler.Organization) {
departments := group.Group("/department")
departments.POST("/getDepartmentList", h.ListDepartments)
departments.POST("/createDepartment", h.CreateDepartment)

View File

@ -2,15 +2,16 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterParameter(group *gin.RouterGroup, handler *Parameter) {
func RegisterParameter(group *gin.RouterGroup, h *handler.Parameter) {
router := group.Group("/sysParams")
router.POST("/createSysParams", handler.Create)
router.PUT("/updateSysParams", handler.Update)
router.DELETE("/deleteSysParams", handler.Delete)
router.DELETE("/deleteSysParamsByIds", handler.DeleteMany)
router.GET("/findSysParams", handler.Find)
router.GET("/getSysParam", handler.Get)
router.GET("/getSysParamsList", handler.List)
router.POST("/createSysParams", h.Create)
router.PUT("/updateSysParams", h.Update)
router.DELETE("/deleteSysParams", h.Delete)
router.DELETE("/deleteSysParamsByIds", h.DeleteMany)
router.GET("/findSysParams", h.Find)
router.GET("/getSysParam", h.Get)
router.GET("/getSysParamsList", h.List)
}

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterPayment(group, public *gin.RouterGroup, h *Payment) {
func RegisterPayment(group, public *gin.RouterGroup, h *handler.Payment) {
payment := group.Group("/payment")
payment.GET("/orders", h.Orders)
payment.POST("/providers/:provider/test", h.TestProvider)

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterPermission(group *gin.RouterGroup, h *Permission) {
func RegisterPermission(group *gin.RouterGroup, h *handler.Permission) {
buttons := group.Group("/authorityBtn")
buttons.POST("/getAuthorityBtn", h.Buttons)
buttons.POST("/setAuthorityBtn", h.SetButtons)

View File

@ -2,13 +2,14 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterPublic(group *gin.RouterGroup, engine *gin.Engine, handler *Public) {
func RegisterPublic(group *gin.RouterGroup, engine *gin.Engine, h *handler.Public) {
base := group.Group("/base")
base.POST("/captcha", handler.Captcha)
base.POST("/login", handler.Login)
base.POST("/captcha", h.Captcha)
base.POST("/login", h.Login)
init := group.Group("/init")
init.POST("/checkdb", handler.CheckDatabase)
init.POST("/initdb", handler.InitializeDatabase(engine))
init.POST("/checkdb", h.CheckDatabase)
init.POST("/initdb", h.InitializeDatabase(engine))
}

View File

@ -1,16 +1,17 @@
package router
import (
"kra/internal/server/handler"
platformmodule "kra/pkg/module"
"github.com/gin-gonic/gin"
)
type Routes struct {
handlers *Set
handlers *handler.Set
}
func NewRoutes(handlers *Set) *Routes { return &Routes{handlers: handlers} }
func NewRoutes(handlers *handler.Set) *Routes { return &Routes{handlers: handlers} }
var _ platformmodule.RouteRegistrar = (*Routes)(nil)

View File

@ -2,16 +2,17 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterSystemConfig(group *gin.RouterGroup, handler *SystemConfig) {
func RegisterSystemConfig(group *gin.RouterGroup, h *handler.SystemConfig) {
security := group.Group("/securityConfig")
security.GET("/getSecurityConfig", handler.GetSecurity)
security.POST("/setSecurityConfig", handler.SetSecurity)
security.GET("/getSecurityConfig", h.GetSecurity)
security.POST("/setSecurityConfig", h.SetSecurity)
system := group.Group("/system")
system.POST("/getSystemConfig", handler.Get)
system.POST("/setSystemConfig", handler.Set)
system.POST("/reloadSystem", handler.Reload)
system.POST("/getServerInfo", handler.ServerInfo)
system.POST("/getSystemConfig", h.Get)
system.POST("/setSystemConfig", h.Set)
system.POST("/reloadSystem", h.Reload)
system.POST("/getServerInfo", h.ServerInfo)
}

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterTask(group *gin.RouterGroup, h *Task) {
func RegisterTask(group *gin.RouterGroup, h *handler.Task) {
router := group.Group("/timedTask")
router.POST("/createTimedTask", h.Create)
router.PUT("/updateTimedTask", h.Update)

View File

@ -2,27 +2,28 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterUser(group *gin.RouterGroup, handler *User) {
func RegisterUser(group *gin.RouterGroup, h *handler.User) {
user := group.Group("/user")
user.POST("/getUserList", handler.List)
user.POST("/admin_register", handler.Create)
user.PUT("/setUserInfo", handler.Update)
user.PUT("/setSelfInfo", handler.UpdateSelf)
user.DELETE("/deleteUser", handler.Delete)
user.POST("/resetPassword", handler.ResetPassword)
user.POST("/changePassword", handler.ChangePassword)
user.PUT("/setSelfSetting", handler.SetSelfSetting)
user.POST("/setUserAuthorities", handler.SetAuthorities)
user.POST("/setUserAuthority", handler.SwitchAuthority)
user.GET("/getUserInfo", handler.Get)
user.POST("/getUserList", h.List)
user.POST("/admin_register", h.Create)
user.PUT("/setUserInfo", h.Update)
user.PUT("/setSelfInfo", h.UpdateSelf)
user.DELETE("/deleteUser", h.Delete)
user.POST("/resetPassword", h.ResetPassword)
user.POST("/changePassword", h.ChangePassword)
user.PUT("/setSelfSetting", h.SetSelfSetting)
user.POST("/setUserAuthorities", h.SetAuthorities)
user.POST("/setUserAuthority", h.SwitchAuthority)
user.GET("/getUserInfo", h.Get)
}
func RegisterNavigation(group *gin.RouterGroup, handler *Navigation) {
group.Group("/menu").POST("/getMenu", handler.Menu)
func RegisterNavigation(group *gin.RouterGroup, h *handler.Navigation) {
group.Group("/menu").POST("/getMenu", h.Menu)
}
func RegisterSession(group *gin.RouterGroup, handler *Session) {
group.Group("/jwt").POST("/jsonInBlacklist", handler.Logout)
func RegisterSession(group *gin.RouterGroup, h *handler.Session) {
group.Group("/jwt").POST("/jsonInBlacklist", h.Logout)
}

View File

@ -2,9 +2,10 @@ package router
import (
"github.com/gin-gonic/gin"
"kra/internal/server/handler"
)
func RegisterVersion(group *gin.RouterGroup, h *Version) {
func RegisterVersion(group *gin.RouterGroup, h *handler.Version) {
router := group.Group("/sysVersion")
router.DELETE("/deleteSysVersion", h.Delete)
router.DELETE("/deleteSysVersionByIds", h.DeleteMany)

View File

@ -1,27 +1,21 @@
package service
// This package is a compatibility facade for the transport layer. Concrete
// implementations live under service/system, service/payment,
// service/integration, and service/task; aliases here preserve the original
// service.* API used by handlers and the composition root.
// This package is the transport layer's entry point into the service tier.
// Concrete implementations live under service/system, service/payment,
// service/integration, and service/task; the aliases here give handlers one
// import instead of four, and Wire composes the module provider sets directly.
import (
"kra/internal/biz/integration"
paymentbiz "kra/internal/biz/payment"
"kra/internal/biz/system"
taskbiz "kra/internal/biz/task"
"kra/internal/service/dto"
integrationservice "kra/internal/service/integration"
paymentservice "kra/internal/service/payment"
systemservice "kra/internal/service/system"
taskservice "kra/internal/service/task"
"kra/internal/utils/routepath"
"github.com/google/wire"
)
type AccessControlService = systemservice.AccessControlService
type AnnouncementInput = systemservice.AnnouncementInput
type AnnouncementService = systemservice.AnnouncementService
type APIService = systemservice.APIService
type AuditService = systemservice.AuditService
@ -32,7 +26,6 @@ type DepartmentService = systemservice.DepartmentService
type DictionaryService = systemservice.DictionaryService
type EmailService = systemservice.EmailService
type ExportService = systemservice.ExportService
type ExportToken = systemservice.ExportToken
type LogViewerService = systemservice.LogViewerService
type MediaService = systemservice.MediaService
type MenuService = systemservice.MenuService
@ -40,7 +33,6 @@ type ParameterService = systemservice.ParameterService
type PermissionService = systemservice.PermissionService
type PositionService = systemservice.PositionService
type SecurityService = systemservice.SecurityService
type PasswordPolicyError = systemservice.PasswordPolicyError
type SystemConfigService = systemservice.SystemConfigService
type TokenService = systemservice.TokenService
type VersionService = systemservice.VersionService
@ -49,90 +41,9 @@ type IntegrationConfigService = integrationservice.IntegrationConfigService
type TaskService = taskservice.TaskService
type UserService = systemservice.UserService
func NewAccessControlService(uc *system.AccessControlUsecase) *AccessControlService {
return systemservice.NewAccessControlService(uc)
}
func NewAnnouncementService(uc *system.AnnouncementUsecase) *AnnouncementService {
return systemservice.NewAnnouncementService(uc)
}
func NewAPIService(uc *system.APIUsecase, settings system.RuntimeSettings) *APIService {
return systemservice.NewAPIService(uc, settings)
}
func NewAuditService(uc *system.AuditUsecase) *AuditService {
return systemservice.NewAuditService(uc)
}
func NewAuditRecorder(uc *system.AuditRecorderUsecase) *AuditRecorder {
return systemservice.NewAuditRecorder(uc)
}
func NewAuthService(uc *system.AuthenticationUsecase) *AuthService {
return systemservice.NewAuthService(uc)
}
func NewAuthorityService(uc *system.AuthorityUsecase) *AuthorityService {
return systemservice.NewAuthorityService(uc)
}
func NewDepartmentService(uc *system.DepartmentUsecase) *DepartmentService {
return systemservice.NewDepartmentService(uc)
}
func NewDictionaryService(uc *system.DictionaryUsecase) *DictionaryService {
return systemservice.NewDictionaryService(uc)
}
func NewEmailService(uc *system.EmailUsecase) *EmailService {
return systemservice.NewEmailService(uc)
}
func NewExportService(uc *system.ExportUsecase, cache system.Cache) *ExportService {
return systemservice.NewExportService(uc, cache)
}
func NewIntegrationConfigService(uc *integration.IntegrationConfigUsecase) *IntegrationConfigService {
return integrationservice.NewIntegrationConfigService(uc)
}
func NewLogViewerService(uc *system.LogViewerUsecase) *LogViewerService {
return systemservice.NewLogViewerService(uc)
}
func NewMediaService(uc *system.MediaUsecase, settings system.RuntimeSettings) *MediaService {
return systemservice.NewMediaService(uc, settings)
}
func NewMenuService(uc *system.MenuUsecase) *MenuService {
return systemservice.NewMenuService(uc)
}
func NewParameterService(uc *system.ParameterUsecase) *ParameterService {
return systemservice.NewParameterService(uc)
}
func NewPaymentService(uc *paymentbiz.PaymentUsecase) *PaymentService {
return paymentservice.NewPaymentService(uc)
}
func NewPermissionService(uc *system.PermissionUsecase) *PermissionService {
return systemservice.NewPermissionService(uc)
}
func NewPositionService(uc *system.PositionUsecase) *PositionService {
return systemservice.NewPositionService(uc)
}
func NewSecurityService(uc *system.SecurityUsecase) *SecurityService {
return systemservice.NewSecurityService(uc)
}
func NewSystemConfigService(uc *system.SystemConfigUsecase, settings system.RuntimeSettings) *SystemConfigService {
return systemservice.NewSystemConfigService(uc, settings)
}
func NewTaskService(uc *taskbiz.TaskApplicationUsecase) *TaskService {
return taskservice.NewTaskService(uc)
}
func NewTokenService(uc *system.TokenUsecase, issuer system.TokenIssuer) *TokenService {
return systemservice.NewTokenService(uc, issuer)
}
func NewUserService(uc *system.UserUsecase, settings *SecurityService) *UserService {
return systemservice.NewUserService(uc, settings)
}
func NewVersionService(uc *system.VersionUsecase) *VersionService {
return systemservice.NewVersionService(uc)
}
var ErrExportTokenInvalid = systemservice.ErrExportTokenInvalid
var ErrExportTokenMalformed = systemservice.ErrExportTokenMalformed
var ErrExportTokenType = systemservice.ErrExportTokenType
func NormalizeRoutePath(path, routerPrefix string) string {
return routepath.Normalize(path, routerPrefix)
}
func IsPasswordPolicyError(err error) bool { return systemservice.IsPasswordPolicyError(err) }
func DefaultPaymentCallbackAck(provider string, success bool) dto.PaymentCallbackAck {

View File

@ -9,14 +9,6 @@ import (
"kra/internal/service/dto"
)
type AnnouncementInput struct {
ID uint
Title string
Content string
UserID *int
Attachments json.RawMessage
}
type AnnouncementService struct{ uc *system.AnnouncementUsecase }
func NewAnnouncementService(uc *system.AnnouncementUsecase) *AnnouncementService {
@ -27,15 +19,15 @@ func announcementDTO(item *system.Announcement) *dto.AnnouncementResponse {
return &dto.AnnouncementResponse{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, DeletedAt: nil, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: json.RawMessage(item.Attachments)}
}
func announcementDO(in AnnouncementInput) *system.Announcement {
func announcementDO(in *dto.AnnouncementRequest) *system.Announcement {
return &system.Announcement{ID: in.ID, Title: in.Title, Content: in.Content, UserID: in.UserID, Attachments: in.Attachments}
}
func (s *AnnouncementService) Create(ctx context.Context, in AnnouncementInput) error {
func (s *AnnouncementService) Create(ctx context.Context, in *dto.AnnouncementRequest) error {
return s.uc.Create(ctx, announcementDO(in))
}
func (s *AnnouncementService) Update(ctx context.Context, in AnnouncementInput) error {
func (s *AnnouncementService) Update(ctx context.Context, in *dto.AnnouncementRequest) error {
return s.uc.Update(ctx, announcementDO(in))
}

View File

@ -56,9 +56,6 @@ func (s *AuditService) DeleteOperations(ctx context.Context, ids []int) error {
func (s *AuditRecorder) RecordLogin(ctx context.Context, v *system.LoginLog) error {
return s.uc.RecordLogin(ctx, v)
}
func (s *AuditRecorder) RecordLoginRequest(ctx context.Context, value *dto.LoginLogRequest) error {
return s.RecordLogin(ctx, &system.LoginLog{Username: value.Username, IP: value.IP, Status: value.Status, ErrorMessage: value.ErrorMessage, Agent: value.Agent, UserID: value.UserID})
}
func (s *AuditService) LoginsFilter(ctx context.Context, page, size int, username string, status bool) ([]*dto.LoginLogResponse, int64, error) {
return s.Logins(ctx, page, size, &system.LoginLog{Username: username, Status: status, FilterByStatus: status})
}
@ -94,9 +91,6 @@ func (s *AuditService) DeleteLogins(ctx context.Context, ids []int) error {
func (s *AuditRecorder) RecordDataAccess(ctx context.Context, v *system.DataAccessLog) error {
return s.uc.RecordDataAccess(ctx, v)
}
func (s *AuditRecorder) RecordDataAccessRequest(ctx context.Context, value *dto.DataAccessRecordRequest) error {
return s.RecordDataAccess(ctx, &system.DataAccessLog{EventType: value.EventType, Operation: value.Operation, UserID: value.UserID, AuthorityID: value.AuthorityID, RequestID: value.RequestID, Method: value.Method, Path: value.Path, Detail: value.Detail})
}
func (s *AuditService) DataAccessRequest(ctx context.Context, req *dto.DataAccessListRequest) ([]*dto.DataAccessLogResponse, int64, error) {
return s.DataAccess(ctx, req.Page, req.PageSize, &system.DataAccessLog{EventType: req.EventType, TargetTable: req.TargetTable})
}

View File

@ -14,7 +14,3 @@ func (s *EmailService) Test(ctx context.Context) error { return s.uc.Test(ctx) }
func (s *EmailService) Send(ctx context.Context, to, subject, body string) error {
return s.uc.Send(ctx, to, subject, body)
}
func (s *EmailService) Alert(ctx context.Context, subject, body string) error {
return s.uc.Alert(ctx, subject, body)
}

View File

@ -64,10 +64,6 @@ func (s *SecurityService) CaptchaSettings() system.CaptchaSettings {
return s.uc.CaptchaRuntimeSettings()
}
func (s *SecurityService) ActiveTokenMatches(ctx context.Context, username, token string) (bool, error) {
return s.uc.ActiveTokenMatches(ctx, username, token)
}
func (s *SecurityService) RotateActiveToken(ctx context.Context, username, oldToken, newToken string, expiration time.Duration) error {
return s.uc.RotateActiveToken(ctx, username, oldToken, newToken, expiration)
}

View File

@ -7,14 +7,6 @@ import (
"kra/internal/service/dto"
)
type userInput struct {
ID uint
Username, Password, NickName, HeaderImg, Phone, Email string
AuthorityID uint
Enable int
AuthorityIDs []uint
}
type UserService struct {
uc *system.UserUsecase
settings *SecurityService
@ -24,28 +16,39 @@ func NewUserService(uc *system.UserUsecase, settings *SecurityService) *UserServ
return &UserService{uc: uc, settings: settings}
}
func userRequestInput(value *dto.UserRequest) userInput {
return userInput{ID: value.ID, Username: value.Username, Password: value.Password, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, AuthorityIDs: value.AuthorityIDs, Enable: value.Enable, Phone: value.Phone, Email: value.Email}
}
func (s *UserService) ListUsersRequest(ctx context.Context, value *dto.UserListRequest) ([]*dto.UserResponse, int64, error) {
return s.ListUsers(ctx, value.Page, value.PageSize, &system.UserListFilter{Username: value.Username, NickName: value.NickName, Phone: value.Phone, Email: value.Email, OrderKey: value.OrderKey, Desc: value.Desc})
}
func (s *UserService) CreateUserRequest(ctx context.Context, value *dto.UserRequest) (*dto.UserResponse, error) {
return s.createUser(ctx, userRequestInput(value))
}
func (s *UserService) UpdateUserRequest(ctx context.Context, value *dto.UserRequest) error {
return s.updateUser(ctx, userRequestInput(value))
}
func (s *UserService) UpdateSelfUserRequest(ctx context.Context, id uint, value *dto.SelfUserRequest) error {
return s.updateSelfUser(ctx, userInput{ID: id, NickName: value.NickName, HeaderImg: value.HeaderImg, Phone: value.Phone, Email: value.Email, Enable: value.Enable})
users, total, err := s.uc.ListUsers(ctx, value.Page, value.PageSize, &system.UserListFilter{Username: value.Username, NickName: value.NickName, Phone: value.Phone, Email: value.Email, OrderKey: value.OrderKey, Desc: value.Desc})
if err != nil {
return nil, 0, err
}
result := make([]*dto.UserResponse, 0, len(users))
for _, user := range users {
result = append(result, convertUser(user))
}
return result, total, nil
}
func (s *UserService) User(ctx context.Context, id uint) (*dto.UserResponse, error) {
value, err := s.uc.User(ctx, id)
func (s *UserService) CreateUserRequest(ctx context.Context, value *dto.UserRequest) (*dto.UserResponse, error) {
if err := s.settings.ValidatePassword(ctx, value.Password); err != nil {
return nil, err
}
security, _ := s.settings.CurrentSecurity(ctx)
mustChange := security != nil && security.ForceNewUserChangePassword
user, err := s.uc.CreateUser(ctx, &system.User{Username: value.Username, Password: value.Password, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, Phone: value.Phone, Email: value.Email, Enable: value.Enable, MustChangePassword: mustChange}, value.AuthorityIDs)
if err != nil {
return nil, err
}
return convertUser(value), nil
return convertUser(user), nil
}
func (s *UserService) UpdateUserRequest(ctx context.Context, value *dto.UserRequest) error {
// The compatible ChangeUserInfo payload uses authorityIds for role assignment; the
// standalone authorityId field is not applied by setUserInfo.
return s.uc.UpdateUser(ctx, &system.User{ID: value.ID, NickName: value.NickName, HeaderImg: value.HeaderImg, Phone: value.Phone, Email: value.Email, Enable: value.Enable}, value.AuthorityIDs)
}
func (s *UserService) UpdateSelfUserRequest(ctx context.Context, id uint, value *dto.SelfUserRequest) error {
return s.uc.UpdateSelfUser(ctx, &system.User{ID: id, NickName: value.NickName, HeaderImg: value.HeaderImg, Phone: value.Phone, Email: value.Email, Enable: value.Enable})
}
func (s *UserService) UserByUUID(ctx context.Context, uuid string) (*dto.UserResponse, error) {
@ -64,18 +67,6 @@ func (s *UserService) Menus(ctx context.Context, authorityID uint) ([]*dto.Dynam
return dynamicMenuResponses(menus), nil
}
func (s *UserService) ListUsers(ctx context.Context, page, pageSize int, filter *system.UserListFilter) ([]*dto.UserResponse, int64, error) {
users, total, err := s.uc.ListUsers(ctx, page, pageSize, filter)
if err != nil {
return nil, 0, err
}
result := make([]*dto.UserResponse, 0, len(users))
for _, user := range users {
result = append(result, convertUser(user))
}
return result, total, nil
}
func (s *UserService) Authorities(ctx context.Context) ([]*dto.AuthorityResponse, error) {
values, err := s.uc.Authorities(ctx)
if err != nil {
@ -88,26 +79,6 @@ func (s *UserService) Authorities(ctx context.Context) ([]*dto.AuthorityResponse
return result, nil
}
func (s *UserService) createUser(ctx context.Context, input userInput) (*dto.UserResponse, error) {
if err := s.settings.ValidatePassword(ctx, input.Password); err != nil {
return nil, err
}
security, _ := s.settings.CurrentSecurity(ctx)
mustChange := security != nil && security.ForceNewUserChangePassword
user, err := s.uc.CreateUser(ctx, &system.User{Username: input.Username, Password: input.Password, NickName: input.NickName, HeaderImg: input.HeaderImg, AuthorityID: input.AuthorityID, Phone: input.Phone, Email: input.Email, Enable: input.Enable, MustChangePassword: mustChange}, input.AuthorityIDs)
if err != nil {
return nil, err
}
return convertUser(user), nil
}
func (s *UserService) updateUser(ctx context.Context, input userInput) error {
// The compatible ChangeUserInfo payload uses authorityIds for role assignment; the
// standalone authorityId field is not applied by setUserInfo.
return s.uc.UpdateUser(ctx, &system.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, Phone: input.Phone, Email: input.Email, Enable: input.Enable}, input.AuthorityIDs)
}
func (s *UserService) updateSelfUser(ctx context.Context, input userInput) error {
return s.uc.UpdateSelfUser(ctx, &system.User{ID: input.ID, NickName: input.NickName, HeaderImg: input.HeaderImg, Phone: input.Phone, Email: input.Email, Enable: input.Enable})
}
func (s *UserService) DeleteUser(ctx context.Context, id uint) error {
return s.uc.DeleteUser(ctx, id)
}