优化结构

This commit is contained in:
Yvan
2026-08-20 20:39:09 +08:00
parent b88c220e20
commit 01b5c2f78a
73 changed files with 3171 additions and 794 deletions
+29
View File
@@ -0,0 +1,29 @@
package system
import "strings"
// IgnoredAPI describes an endpoint excluded from the generated root policy.
// It deliberately has no GORM tags so the system module can be reused by
// bootstrap and migration code without exposing data-layer PO types.
type IgnoredAPI struct {
Method string
Path string
}
func DefaultIgnoredAPIs(staticPath string) []IgnoredAPI {
staticRoute := "/" + strings.Trim(staticPath, "/") + "/*filepath"
return []IgnoredAPI{
{Method: "GET", Path: "/api/freshCasbin"},
{Method: "GET", Path: "/health"},
{Method: "GET", Path: "/swagger/*any"},
{Method: "GET", Path: staticRoute},
{Method: "HEAD", Path: staticRoute},
{Method: "POST", Path: "/system/reloadSystem"},
{Method: "POST", Path: "/base/login"},
{Method: "POST", Path: "/base/captcha"},
{Method: "POST", Path: "/init/initdb"},
{Method: "POST", Path: "/init/checkdb"},
{Method: "GET", Path: "/info/getInfoDataSource"},
{Method: "GET", Path: "/info/getInfoPublic"},
}
}
+148
View File
@@ -0,0 +1,148 @@
// Package system contains data-layer implementations for system runtime
// settings, authentication tokens, and bootstrap definitions.
package system
import (
"errors"
"time"
"kra/internal/biz"
"kra/internal/conf"
"kra/pkg/adminauth"
jwt "github.com/golang-jwt/jwt/v5"
)
type runtimeSettings struct{ runtime *conf.Runtime }
func NewRuntimeSettings(runtime *conf.Runtime) biz.RuntimeSettings {
return &runtimeSettings{runtime: runtime}
}
func (s *runtimeSettings) RouterPrefix() string {
config := s.runtime.Admin()
if config == nil {
return ""
}
return config.RouterPrefix
}
func (s *runtimeSettings) JWTSettings() biz.JWTSettings {
value := biz.JWTSettings{Issuer: "kra", Expires: 7 * 24 * time.Hour, Buffer: 24 * time.Hour}
config := s.runtime.Admin()
if config == nil || config.Jwt == nil {
return value
}
value.SigningKey = config.Jwt.SigningKey
if config.Jwt.Issuer != "" {
value.Issuer = config.Jwt.Issuer
}
if config.Jwt.ExpiresTime != nil {
value.Expires = config.Jwt.ExpiresTime.AsDuration()
}
if config.Jwt.BufferTime != nil {
value.Buffer = config.Jwt.BufferTime.AsDuration()
}
return value
}
func (s *runtimeSettings) CaptchaSettings() biz.CaptchaSettings {
value := biz.CaptchaSettings{KeyLong: 6, ImageWidth: 240, ImageHeight: 80, StoreExpiration: 3 * time.Minute}
config := s.runtime.Admin()
if config == nil || config.Captcha == nil {
return value
}
if config.Captcha.KeyLong > 0 {
value.KeyLong = int(config.Captcha.KeyLong)
}
if config.Captcha.ImgWidth > 0 {
value.ImageWidth = int(config.Captcha.ImgWidth)
}
if config.Captcha.ImgHeight > 0 {
value.ImageHeight = int(config.Captcha.ImgHeight)
}
if config.Captcha.StoreExpiration != nil {
value.StoreExpiration = config.Captcha.StoreExpiration.AsDuration()
}
return value
}
func (s *runtimeSettings) MediaSettings() biz.MediaSettings {
config := s.runtime.Admin()
if config == nil || config.Media == nil {
return biz.MediaSettings{}
}
return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize, ChunkDir: config.Media.ChunkDir}
}
func (s *runtimeSettings) UseMultipoint() bool {
config := s.runtime.Admin()
return config != nil && config.System != nil && config.System.UseMultipoint
}
type tokenIssuer struct{ settings biz.RuntimeSettings }
func NewTokenIssuer(settings biz.RuntimeSettings) biz.TokenIssuer {
return &tokenIssuer{settings: settings}
}
func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePassword bool, expires time.Duration) (*biz.IssuedToken, error) {
settings := i.settings.JWTSettings()
if expires <= 0 {
expires = settings.Expires
}
passwordVersion := int64(0)
if user.PasswordUpdatedAt != nil {
passwordVersion = user.PasswordUpdatedAt.UnixNano()
}
token, claims, err := adminauth.Generate(settings.SigningKey, settings.Issuer, expires, settings.Buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, mustChangePassword, passwordVersion)
if err != nil {
return nil, err
}
return &biz.IssuedToken{Value: token, ExpiresAt: claims.ExpiresAt.Time, TTL: expires}, nil
}
func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*biz.IssuedToken, error) {
if source == nil {
return nil, errors.New("nil JWT claims")
}
settings := i.settings.JWTSettings()
claims := &adminauth.Claims{
UUID: source.UUID, ID: source.ID, Username: source.Username, NickName: source.NickName,
AuthorityID: authorityID, BufferTime: int64(source.BufferTime / time.Second), UserType: source.UserType,
MustChangePwd: source.MustChangePwd, PasswordVersion: source.PasswordVersion,
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings(append([]string(nil), source.Audience...)), Issuer: source.Issuer,
IssuedAt: jwt.NewNumericDate(source.IssuedAt), NotBefore: jwt.NewNumericDate(source.NotBefore), ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
},
}
token, err := adminauth.Sign(settings.SigningKey, claims)
if err != nil {
return nil, err
}
return &biz.IssuedToken{Value: token, ExpiresAt: source.ExpiresAt, TTL: time.Until(source.ExpiresAt)}, 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
}
}
audience := append([]string(nil), claims.Audience...)
issuedAt := time.Time{}
if claims.IssuedAt != nil {
issuedAt = claims.IssuedAt.Time
}
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
}
+94
View File
@@ -0,0 +1,94 @@
package system
import (
"context"
"errors"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
// DatabaseProvider is the smaller seam used by security settings, which also
// need to work before the primary database is initialized.
type DatabaseProvider interface {
DB() *gorm.DB
DatabaseReady() bool
}
type SecurityConfigPO struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
CaptchaOpen int `gorm:"default:0"`
CaptchaTimeout int `gorm:"default:3600"`
KeyLong int `gorm:"default:6"`
ImgWidth int `gorm:"default:240"`
ImgHeight int `gorm:"default:80"`
PwdMinLength int `gorm:"default:8"`
PwdRequireUpper bool `gorm:"default:false"`
PwdRequireLower bool `gorm:"default:false"`
PwdRequireDigit bool `gorm:"default:false"`
PwdRequireSpecial bool `gorm:"default:false"`
LimitEnable bool `gorm:"default:false"`
LimitWindow int `gorm:"default:60"`
LimitCount int `gorm:"default:30"`
LockEnable bool `gorm:"default:false"`
LockThreshold int `gorm:"default:5"`
LockDuration int `gorm:"default:30"`
PwdExpireEnable bool `gorm:"default:false"`
PwdExpireDays int `gorm:"default:90"`
ForceNewUserChangePassword bool `gorm:"default:false"`
}
func (SecurityConfigPO) TableName() string { return "sys_security_config" }
func DefaultSecurityConfig() SecurityConfigPO {
return SecurityConfigPO{ID: 1, CaptchaTimeout: 3600, KeyLong: 6, ImgWidth: 240, ImgHeight: 80, PwdMinLength: 8, LimitWindow: 60, LimitCount: 30, LockThreshold: 5, LockDuration: 30, PwdExpireDays: 90}
}
type securityRepo struct{ data DatabaseProvider }
func NewSecurityRepo(data DatabaseProvider) biz.SecurityRepo { return &securityRepo{data: data} }
func securityFromPO(v SecurityConfigPO) *biz.SecurityConfig {
return &biz.SecurityConfig{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func securityToPO(v *biz.SecurityConfig) SecurityConfigPO {
return SecurityConfigPO{ID: 1, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
if !r.data.DatabaseReady() {
po := DefaultSecurityConfig()
po.ID = 0
return securityFromPO(po), errors.New("数据库未初始化")
}
db := r.data.DB().WithContext(ctx)
var po SecurityConfigPO
err := db.First(&po, 1).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
po = DefaultSecurityConfig()
err = db.Create(&po).Error
}
if err != nil {
return nil, err
}
return securityFromPO(po), nil
}
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
po := securityToPO(v)
if err := r.data.DB().WithContext(ctx).Save(&po).Error; err != nil {
return err
}
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
return nil
}
func (r *securityRepo) BackfillPasswordUpdatedAt(ctx context.Context, at time.Time) error {
return r.data.DB().WithContext(ctx).Table("sys_users").Where("password_updated_at IS NULL").Update("password_updated_at", at).Error
}