feat: complete Kra admin backend migration

This commit is contained in:
2026-08-14 00:28:37 +08:00
parent c4bf7ee03b
commit acba9624d7
88 changed files with 1765 additions and 8623 deletions
+71 -22
View File
@@ -34,14 +34,6 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(ginRequestMeta(), emailErrorAlert(emails, logger), gin.Recovery(), securityRateLimit(svc, settings), operationAudit(audit))
localStorage := config == nil || config.Storage == nil || config.Storage.Type == "" || config.Storage.Type == "local"
if localStorage && config != nil && config.Local != nil && config.Local.StorePath != "" {
pathPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
if pathPrefix != "/" {
engine.StaticFS(pathPrefix, filesOnly{FileSystem: http.Dir(config.Local.StorePath)})
}
}
prefix := ""
if config != nil {
prefix = strings.TrimSuffix(config.RouterPrefix, "/")
@@ -50,13 +42,13 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
public.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
registerPublicRoutes(public, engine, config, svc, settings, access, audit)
registerPublicRoutes(public, engine, config, svc, settings, access, audit, scheduler)
private := engine.Group(prefix)
private.Use(jwtAuth(config, settings), mustChangePasswordGuard(), accessControl(access, audit))
registerPrivateRoutes(private, svc)
registerPrivateRoutes(private, svc, settings)
registerAccessRoutes(private, engine, access)
registerSettingsRoutes(private, public, settings)
registerSystemConfigRoutes(private, config, svc, settings)
registerSystemConfigRoutes(private, config, svc, settings, scheduler)
registerVersionRoutes(private, versions)
registerExportRoutes(private, public, svc, exports)
registerAuditRoutes(private, public, audit)
@@ -66,6 +58,9 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
registerEmailRoutes(private, emails)
engine.NoRoute(func(c *gin.Context) {
if serveLocalStorage(c, config) {
return
}
fail(c, "请求的接口不存在")
})
@@ -88,6 +83,23 @@ func NewGinServer(c *conf.Server, config *conf.AdminBackend, svc *service.System
}
}
// serveLocalStorage resolves the local path for every request so a config
// reload takes effect without rebuilding the Gin engine.
func serveLocalStorage(c *gin.Context, config *conf.AdminBackend) bool {
if config == nil || config.Local == nil || config.Local.StorePath == "" {
return false
}
if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
return false
}
prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
if prefix == "/" || (c.Request.URL.Path != prefix && !strings.HasPrefix(c.Request.URL.Path, prefix+"/")) {
return false
}
http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.Local.StorePath)})).ServeHTTP(c.Writer, c.Request)
return true
}
func jwtAuth(config *conf.AdminBackend, settings *service.SettingsService) gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("x-token")
@@ -107,6 +119,27 @@ func jwtAuth(config *conf.AdminBackend, settings *service.SettingsService) gin.H
noAuth(c, "登录状态已失效")
return
}
if claims.ExpiresAt != nil && claims.BufferTime > 0 && time.Until(claims.ExpiresAt.Time) < time.Duration(claims.BufferTime)*time.Second {
expires, buffer := 7*24*time.Hour, time.Duration(claims.BufferTime)*time.Second
issuer := claims.Issuer
if config != nil && config.Jwt != nil {
if config.Jwt.ExpiresTime != nil {
expires = config.Jwt.ExpiresTime.AsDuration()
}
if config.Jwt.BufferTime != nil {
buffer = config.Jwt.BufferTime.AsDuration()
}
if config.Jwt.Issuer != "" {
issuer = config.Jwt.Issuer
}
}
newToken, newClaims, refreshErr := adminauth.Generate(secret, issuer, expires, buffer, claims.ID, claims.AuthorityID, claims.UUID, claims.Username, claims.NickName, claims.MustChangePwd)
if refreshErr == nil {
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: newToken, Path: "/", MaxAge: int(expires.Seconds()), HttpOnly: true, SameSite: http.SameSiteStrictMode})
}
}
c.Set("admin_claims", claims)
c.Next()
}
@@ -134,7 +167,7 @@ func mustChangePasswordGuard() gin.HandlerFunc {
}
}
func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *conf.AdminBackend, svc *service.SystemService, settings *service.SettingsService, access *service.AccessService, audit *service.AuditService) {
func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *conf.AdminBackend, svc *service.SystemService, settings *service.SettingsService, access *service.AccessService, audit *service.AuditService, scheduler *TaskScheduler) {
expiration := 3 * time.Minute
keyLong, width, height := 6, 240, 80
if config != nil && config.Captcha != nil {
@@ -238,22 +271,31 @@ func registerPublicRoutes(group *gin.RouterGroup, engine *gin.Engine, config *co
writeResult(c, codeSuccess, gin.H{"needInit": !initialized}, message)
})
initGroup.POST("/initdb", func(c *gin.Context) {
initialized, err := svc.IsInitialized(c.Request.Context())
if err != nil {
fail(c, "数据库状态检查失败")
return
}
if initialized {
fail(c, "数据库已初始化,无需重复初始化")
return
}
var input service.DatabaseInit
if err := c.ShouldBindJSON(&input); err != nil {
fail(c, "数据库初始化参数无效")
return
}
if err := svc.Initialize(c.Request.Context(), &input); err != nil {
fail(c, "自动创建数据库失败: "+err.Error())
return
}
routes := engine.Routes()
apis := make([]*biz.API, 0, len(routes))
for _, route := range routes {
apis = append(apis, &biz.API{Path: route.Path, Method: route.Method, APIGroup: routeGroup(route.Path)})
}
if err := access.ApplyAPISync(c.Request.Context(), apis, nil); err != nil {
fail(c, "数据库已初始化,但 API 权限数据写入失败")
if err := svc.Initialize(c.Request.Context(), &input, apis); err != nil {
fail(c, "自动创建数据库失败: "+err.Error())
return
}
if err := scheduler.Reload(c.Request.Context()); err != nil {
fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error())
return
}
writeResult(c, codeSuccess, gin.H{}, "自动创建数据库成功")
@@ -277,9 +319,8 @@ func cachedInt(ctx context.Context, svc *service.SystemService, key string) (int
return result, err
}
func incrementCached(ctx context.Context, svc *service.SystemService, key string, ttl time.Duration) (int, error) {
value, _ := cachedInt(ctx, svc, key)
value++
return value, svc.CacheSet(ctx, key, strconv.Itoa(value), ttl)
value, err := svc.CacheIncrement(ctx, key, ttl)
return int(value), err
}
func securityRateLimit(svc *service.SystemService, settings *service.SettingsService) gin.HandlerFunc {
return func(c *gin.Context) {
@@ -356,7 +397,7 @@ func (f filesOnly) Open(name string) (http.File, error) {
return file, nil
}
func registerPrivateRoutes(group *gin.RouterGroup, svc *service.SystemService) {
func registerPrivateRoutes(group *gin.RouterGroup, svc *service.SystemService, settings *service.SettingsService) {
user := group.Group("/user")
user.POST("/getUserList", func(c *gin.Context) {
var req struct {
@@ -572,6 +613,14 @@ func registerPrivateRoutes(group *gin.RouterGroup, svc *service.SystemService) {
})
jwt := group.Group("/jwt")
jwt.POST("/jsonInBlacklist", func(c *gin.Context) {
token := c.GetHeader("x-token")
if token == "" {
token, _ = c.Cookie("x-token")
}
if err := settings.BlacklistToken(c.Request.Context(), token); err != nil {
fail(c, "退出失败")
return
}
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: "", Path: "/", MaxAge: -1, HttpOnly: true})
ok(c)
})