This commit is contained in:
2026-08-15 03:14:56 +08:00
parent a34133cce5
commit 2a3c55bb53
71 changed files with 1445 additions and 1076 deletions
-15
View File
@@ -1,15 +0,0 @@
package data
import "kra/internal/biz"
type authorityAccessRepo struct{ data *Data }
type apiRepo struct{ data *Data }
type permissionRepo struct{ data *Data }
func NewAuthorityAccessRepo(data *Data) biz.AuthorityAccessRepo {
return &authorityAccessRepo{data: data}
}
func NewAPIRepo(data *Data) biz.APIRepo { return &apiRepo{data: data} }
func NewPermissionRepo(data *Data) biz.PermissionRepo { return &permissionRepo{data: data} }
+4
View File
@@ -14,6 +14,10 @@ import (
"gorm.io/gorm"
)
type apiRepo struct{ data *Data }
func NewAPIRepo(data *Data) biz.APIRepo { return &apiRepo{data: data} }
type apiPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
+5 -1
View File
@@ -10,6 +10,10 @@ import (
"gorm.io/gorm"
)
type apiTokenRepo struct{ data *Data }
func NewAPITokenRepo(data *Data) biz.APITokenRepo { return &apiTokenRepo{data: data} }
type apiTokenPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
@@ -45,7 +49,7 @@ func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID
if err != nil {
return nil, false, err
}
user, err := (&userRepo{data: r.data}).toBizUser(ctx, &po)
user, err := (&userRepo{data: r.data}).loadUser(ctx, &po)
return user, count > 0 || po.AuthorityID == authorityID, err
}
func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error {
+6
View File
@@ -11,6 +11,12 @@ import (
"gorm.io/gorm"
)
type authorityAccessRepo struct{ data *Data }
func NewAuthorityAccessRepo(data *Data) biz.AuthorityAccessRepo {
return &authorityAccessRepo{data: data}
}
func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID uint) (map[uint]bool, error) {
allowed := make(map[uint]bool)
if actorID == 0 {
+303
View File
@@ -0,0 +1,303 @@
package data
import (
"context"
"encoding/json"
"kra/internal/conf"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
)
type configurationEnvelope struct {
Data json.RawMessage `json:"data"`
Admin json.RawMessage `json:"admin"`
Email *struct {
To string `json:"to"`
From string `json:"from"`
Host string `json:"host"`
Secret string `json:"secret"`
Nickname string `json:"nickname"`
Port int32 `json:"port"`
IsSSL bool `json:"is-ssl"`
IsLoginAuth bool `json:"is-loginauth"`
} `json:"email"`
}
func (r *initializationRepo) ConfigurationJSON() (json.RawMessage, error) {
dataConfig, adminConfig := r.data.runtime.Values()
admin := map[string]any{"routerPrefix": ""}
email := map[string]any{}
if adminConfig != nil {
admin["routerPrefix"] = adminConfig.RouterPrefix
if adminConfig.System != nil {
admin["system"] = map[string]any{"useRedis": adminConfig.System.UseRedis, "useMultipoint": adminConfig.System.UseMultipoint, "useStrictAuth": adminConfig.System.UseStrictAuth, "disableAutoMigrate": adminConfig.System.DisableAutoMigrate, "useMongo": adminConfig.System.UseMongo}
}
if adminConfig.Jwt != nil {
admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": durationString(adminConfig.Jwt.ExpiresTime), "bufferTime": durationString(adminConfig.Jwt.BufferTime), "issuer": adminConfig.Jwt.Issuer}
}
if adminConfig.Captcha != nil {
admin["captcha"] = map[string]any{"keyLong": adminConfig.Captcha.KeyLong, "imgWidth": adminConfig.Captcha.ImgWidth, "imgHeight": adminConfig.Captcha.ImgHeight, "storeExpiration": durationString(adminConfig.Captcha.StoreExpiration)}
}
if adminConfig.Local != nil {
admin["local"] = map[string]any{"storePath": adminConfig.Local.StorePath, "pathPrefix": adminConfig.Local.PathPrefix}
}
if adminConfig.Media != nil {
admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize}
}
if adminConfig.Email != nil {
email = map[string]any{"to": adminConfig.Email.To, "from": adminConfig.Email.From, "host": adminConfig.Email.Host, "secret": "******", "nickname": adminConfig.Email.Nickname, "port": adminConfig.Email.Port, "is-ssl": adminConfig.Email.IsSsl, "is-loginauth": adminConfig.Email.IsLoginAuth}
}
if adminConfig.Storage != nil {
storage := proto.Clone(adminConfig.Storage).(*conf.AdminBackend_Storage)
maskStorageSecrets(storage)
admin["storage"] = storage
}
if adminConfig.Zap != nil {
admin["zap"] = adminConfig.Zap
}
if adminConfig.Cors != nil {
admin["cors"] = adminConfig.Cors
}
if adminConfig.App != nil {
admin["app"] = adminConfig.App
}
}
maskDataSecrets(dataConfig)
dataMap := map[string]any{}
if dataConfig != nil {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(dataConfig)
if err != nil {
return nil, err
}
if err = json.Unmarshal(raw, &dataMap); err != nil {
return nil, err
}
}
return json.Marshal(map[string]any{"config": map[string]any{"admin": admin, "email": email, "data": dataMap}})
}
func (r *initializationRepo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error {
currentData, currentAdmin := r.data.runtime.Values()
if currentAdmin == nil {
return nil
}
nextData := cloneDataConfig(currentData)
nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
var value configurationEnvelope
if err := json.Unmarshal(raw, &value); err != nil {
return err
}
options := protojson.UnmarshalOptions{DiscardUnknown: true}
if len(value.Data) > 0 && string(value.Data) != "null" {
patch := &conf.Data{}
if err := options.Unmarshal(value.Data, patch); err != nil {
return err
}
applyDataPatch(nextData, patch)
}
if len(value.Admin) > 0 && string(value.Admin) != "null" {
patch := &conf.AdminBackend{}
if err := options.Unmarshal(value.Admin, patch); err != nil {
return err
}
applyAdminPatch(nextAdmin, patch)
}
preserveDataSecrets(nextData, currentData)
preserveAdminSecrets(nextAdmin, currentAdmin)
if value.Email != nil {
if nextAdmin.Email == nil {
nextAdmin.Email = &conf.AdminBackend_Email{}
}
nextAdmin.Email.To, nextAdmin.Email.From, nextAdmin.Email.Host = value.Email.To, value.Email.From, value.Email.Host
nextAdmin.Email.Nickname, nextAdmin.Email.Port = value.Email.Nickname, value.Email.Port
nextAdmin.Email.IsSsl, nextAdmin.Email.IsLoginAuth = value.Email.IsSSL, value.Email.IsLoginAuth
if value.Email.Secret != "" && value.Email.Secret != "******" {
nextAdmin.Email.Secret = value.Email.Secret
}
}
nextAdmin.ConfigPath = currentAdmin.ConfigPath
dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextData)
if err != nil {
return err
}
adminRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextAdmin)
if err != nil {
return err
}
return r.PersistRuntimeConfig(ctx, dataRaw, adminRaw)
}
func (r *initializationRepo) DiskMountPoints() []string {
config := r.data.runtime.Admin()
if config == nil {
return nil
}
points := make([]string, 0, len(config.DiskList))
for _, item := range config.DiskList {
if item != nil && item.MountPoint != "" {
points = append(points, item.MountPoint)
}
}
return points
}
func cloneDataConfig(value *conf.Data) *conf.Data {
if value == nil {
return &conf.Data{}
}
return proto.Clone(value).(*conf.Data)
}
func durationString(value *durationpb.Duration) string {
if value == nil {
return "0s"
}
return value.AsDuration().String()
}
func applyDataPatch(target, patch *conf.Data) {
if patch.Database != nil {
target.Database = patch.Database
}
if patch.Redis != nil {
target.Redis = patch.Redis
}
if patch.Mongo != nil {
target.Mongo = patch.Mongo
}
if patch.DatabaseList != nil {
target.DatabaseList = patch.DatabaseList
}
if patch.RedisList != nil {
target.RedisList = patch.RedisList
}
}
func applyAdminPatch(target, patch *conf.AdminBackend) {
target.RouterPrefix = patch.RouterPrefix
if patch.System != nil {
target.System = patch.System
}
if patch.Jwt != nil {
target.Jwt = patch.Jwt
}
if patch.Captcha != nil {
target.Captcha = patch.Captcha
}
if patch.Local != nil {
target.Local = patch.Local
}
if patch.Media != nil {
target.Media = patch.Media
}
if patch.Storage != nil {
target.Storage = patch.Storage
}
if patch.Zap != nil {
target.Zap = patch.Zap
}
if patch.Cors != nil {
target.Cors = patch.Cors
}
if patch.App != nil {
target.App = patch.App
}
}
func maskDataSecrets(value *conf.Data) {
if value == nil {
return
}
if value.Database != nil {
value.Database.Password = "******"
}
if value.Redis != nil {
value.Redis.Password = "******"
}
if value.Mongo != nil {
value.Mongo.Password = "******"
}
for _, item := range value.DatabaseList {
if item != nil {
item.Password = "******"
}
}
for _, item := range value.RedisList {
if item != nil {
item.Password = "******"
}
}
}
func preserveDataSecrets(next, current *conf.Data) {
if next == nil || current == nil {
return
}
if next.Database != nil && current.Database != nil && maskedSecret(next.Database.Password) {
next.Database.Password = current.Database.Password
}
if next.Redis != nil && current.Redis != nil && maskedSecret(next.Redis.Password) {
next.Redis.Password = current.Redis.Password
}
if next.Mongo != nil && current.Mongo != nil && maskedSecret(next.Mongo.Password) {
next.Mongo.Password = current.Mongo.Password
}
for i, item := range next.DatabaseList {
if item != nil && i < len(current.DatabaseList) && current.DatabaseList[i] != nil && maskedSecret(item.Password) {
item.Password = current.DatabaseList[i].Password
}
}
for i, item := range next.RedisList {
if item != nil && i < len(current.RedisList) && current.RedisList[i] != nil && maskedSecret(item.Password) {
item.Password = current.RedisList[i].Password
}
}
}
func preserveAdminSecrets(next, current *conf.AdminBackend) {
if next.Jwt != nil && current.Jwt != nil && maskedSecret(next.Jwt.SigningKey) {
next.Jwt.SigningKey = current.Jwt.SigningKey
}
if next.Email != nil && current.Email != nil && maskedSecret(next.Email.Secret) {
next.Email.Secret = current.Email.Secret
}
preserveStorageSecrets(next.Storage, current.Storage)
}
func maskedSecret(value string) bool { return value == "" || value == "******" }
func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore {
if storage == nil {
return nil
}
return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio}
}
func maskStorageSecrets(storage *conf.AdminBackend_Storage) {
if storage.Qiniu != nil {
storage.Qiniu.SecretKey = "******"
}
for _, item := range objectStores(storage) {
if item != nil {
item.SecretKey = "******"
}
}
}
func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) {
if next == nil || current == nil {
return
}
if next.Qiniu != nil && current.Qiniu != nil && maskedSecret(next.Qiniu.SecretKey) {
next.Qiniu.SecretKey = current.Qiniu.SecretKey
}
nextItems, currentItems := objectStores(next), objectStores(current)
for i := range nextItems {
if nextItems[i] != nil && currentItems[i] != nil && maskedSecret(nextItems[i].SecretKey) {
nextItems[i].SecretKey = currentItems[i].SecretKey
}
}
}
+4
View File
@@ -12,6 +12,10 @@ import (
"gorm.io/gorm"
)
type departmentRepo struct{ data *Data }
func NewDepartmentRepo(data *Data) biz.DepartmentRepo { return &departmentRepo{data: data} }
type departmentPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
+4
View File
@@ -12,6 +12,10 @@ import (
"gorm.io/gorm"
)
type dictionaryRepo struct{ data *Data }
func NewDictionaryRepo(data *Data) biz.DictionaryRepo { return &dictionaryRepo{data: data} }
type dictionaryPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
-10
View File
@@ -1,10 +0,0 @@
package data
import "kra/internal/biz"
type departmentRepo struct{ data *Data }
type positionRepo struct{ data *Data }
func NewDepartmentRepo(data *Data) biz.DepartmentRepo { return &departmentRepo{data: data} }
func NewPositionRepo(data *Data) biz.PositionRepo { return &positionRepo{data: data} }
+4
View File
@@ -10,6 +10,10 @@ import (
"gorm.io/gorm/clause"
)
type parameterRepo struct{ data *Data }
func NewParameterRepo(data *Data) biz.ParameterRepo { return &parameterRepo{data: data} }
type parameterPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
+4
View File
@@ -8,6 +8,10 @@ import (
"gorm.io/gorm"
)
type permissionRepo struct{ data *Data }
func NewPermissionRepo(data *Data) biz.PermissionRepo { return &permissionRepo{data: data} }
type menuButtonPO struct {
ID uint `gorm:"primaryKey"`
Name string
+4
View File
@@ -10,6 +10,10 @@ import (
"gorm.io/gorm"
)
type positionRepo struct{ data *Data }
func NewPositionRepo(data *Data) biz.PositionRepo { return &positionRepo{data: data} }
type positionPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
+20
View File
@@ -1,6 +1,7 @@
package data
import (
"errors"
"time"
"kra/internal/biz"
@@ -92,3 +93,22 @@ func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePas
}
return &biz.IssuedToken{Value: token, ExpiresAt: claims.ExpiresAt.Time, TTL: expires}, nil
}
func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
claims, err := adminauth.Parse(token, i.settings.JWTSettings().SigningKey)
if err != nil {
switch {
case errors.Is(err, adminauth.ErrTokenExpired):
return nil, biz.ErrTokenExpired
case errors.Is(err, adminauth.ErrTokenMalformed):
return nil, biz.ErrTokenMalformed
case errors.Is(err, adminauth.ErrTokenSignatureInvalid):
return nil, biz.ErrTokenSignatureInvalid
case errors.Is(err, adminauth.ErrTokenNotValidYet):
return nil, biz.ErrTokenNotValidYet
default:
return nil, biz.ErrTokenInvalid
}
}
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, Issuer: claims.Issuer, ExpiresAt: claims.ExpiresAt.Time}, nil
}
+4
View File
@@ -10,6 +10,10 @@ import (
"gorm.io/gorm"
)
type securityRepo struct{ data *Data }
func NewSecurityRepo(data *Data) biz.SecurityRepo { return &securityRepo{data: data} }
type securityConfigPO struct {
ID uint `gorm:"primaryKey"`
CaptchaOpen int
-16
View File
@@ -1,16 +0,0 @@
package data
import "kra/internal/biz"
type dictionaryRepo struct{ data *Data }
type parameterRepo struct{ data *Data }
type apiTokenRepo struct{ data *Data }
type securityRepo struct{ data *Data }
func NewDictionaryRepo(data *Data) biz.DictionaryRepo { return &dictionaryRepo{data: data} }
func NewParameterRepo(data *Data) biz.ParameterRepo { return &parameterRepo{data: data} }
func NewAPITokenRepo(data *Data) biz.APITokenRepo { return &apiTokenRepo{data: data} }
func NewSecurityRepo(data *Data) biz.SecurityRepo { return &securityRepo{data: data} }
+5 -5
View File
@@ -22,7 +22,7 @@ func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*bi
}
return nil, err
}
return r.toBizUser(ctx, &po)
return r.loadUser(ctx, &po)
}
func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) {
@@ -33,10 +33,10 @@ func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error)
}
return nil, err
}
return r.toBizUser(ctx, &po)
return r.loadUser(ctx, &po)
}
func (r *userRepo) toBizUser(ctx context.Context, po *userPO) (*biz.User, error) {
func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*biz.User, error) {
var authority authorityPO
if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil {
return nil, err
@@ -90,7 +90,7 @@ func baseBizUser(po *userPO) *biz.User {
return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}
}
func (r *userRepo) toBizUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) {
func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) {
if len(pos) == 0 {
return []*biz.User{}, nil
}
@@ -275,7 +275,7 @@ func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *bi
if err := applyPagination(db.Order(order), page, pageSize, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
users, err := r.toBizUsers(ctx, pos)
users, err := r.loadUsers(ctx, pos)
return users, total, err
}