优化结构
This commit is contained in:
@@ -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"},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user