This commit is contained in:
2026-08-15 02:35:23 +08:00
parent bd5dbf2522
commit a34133cce5
49 changed files with 929 additions and 504 deletions
+1 -1
View File
@@ -173,7 +173,7 @@ func (h *Media) InitUpload(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
if config := h.service.MediaConfig(); config != nil && config.MaxFileSize > 0 && req.FileSize > config.MaxFileSize {
if config := h.service.MediaConfig(); config.MaxFileSize > 0 && req.FileSize > config.MaxFileSize {
httpx.Fail(c, "文件超过大小上限")
return
}
+33 -80
View File
@@ -8,45 +8,28 @@ import (
"time"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"kra/internal/worker"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
)
type Public struct {
auth *service.AuthService
system *service.SystemConfigService
settings *service.SecurityService
audit *service.AuditRecorder
scheduler *worker.TaskScheduler
runtime *conf.Runtime
store *captchaStore
auth *service.AuthService
system *service.SystemConfigService
settings *service.SecurityService
store *captchaStore
}
func NewPublic(runtime *conf.Runtime, auth *service.AuthService, system *service.SystemConfigService, settings *service.SecurityService, audit *service.AuditRecorder, scheduler *worker.TaskScheduler) *Public {
return &Public{auth: auth, system: system, settings: settings, audit: audit, scheduler: scheduler, runtime: runtime, store: &captchaStore{service: settings, runtime: runtime}}
func NewPublic(auth *service.AuthService, system *service.SystemConfigService, settings *service.SecurityService) *Public {
return &Public{auth: auth, system: system, settings: settings, store: &captchaStore{service: settings}}
}
func (h *Public) captchaConfig() (int, int, int) {
keyLong, width, height := 6, 240, 80
config := h.runtime.Admin()
if config != nil && config.Captcha != nil {
if config.Captcha.KeyLong > 0 {
keyLong = int(config.Captcha.KeyLong)
}
if config.Captcha.ImgWidth > 0 {
width = int(config.Captcha.ImgWidth)
}
if config.Captcha.ImgHeight > 0 {
height = int(config.Captcha.ImgHeight)
}
}
return keyLong, width, height
config := h.settings.CaptchaSettings()
return config.KeyLong, config.ImageWidth, config.ImageHeight
}
func (h *Public) Captcha(c *gin.Context) {
@@ -74,57 +57,32 @@ func (h *Public) Login(c *gin.Context) {
httpx.Fail(c, "请输入用户名和密码")
return
}
security, _ := h.settings.CurrentSecurity(c.Request.Context())
if security != nil && security.LockEnable {
if locked, _ := h.settings.LoginLocked(c.Request.Context(), req.Username); locked {
httpx.Fail(c, "账号已锁定,请 "+strconv.Itoa(security.LockDuration)+" 分钟后再试")
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "账号已锁定", Agent: c.Request.UserAgent()})
return
}
}
requireCaptcha := security == nil || security.CaptchaOpen == 0
ipTTL := time.Hour
if security != nil {
if security.CaptchaTimeout > 0 {
ipTTL = time.Duration(security.CaptchaTimeout) * time.Second
}
failures, _ := h.settings.EnsureLoginIPCounter(c.Request.Context(), c.ClientIP(), ipTTL)
requireCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
}
if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !h.store.Verify(req.CaptchaID, req.Captcha, true)) {
_, _ = h.settings.IncrementLoginIP(c.Request.Context(), c.ClientIP(), ipTTL)
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "验证码错误", Agent: c.Request.UserAgent()})
httpx.Fail(c, "验证码错误")
return
}
result, err := h.auth.Login(c.Request.Context(), req.Username, req.Password)
result, err := h.auth.Login(c.Request.Context(), &req, c.ClientIP(), c.Request.UserAgent())
if err != nil {
_, _ = h.settings.IncrementLoginIP(c.Request.Context(), c.ClientIP(), ipTTL)
if errors.Is(err, biz.ErrUserDisabled) {
var disabled *service.UserDisabledError
errors.As(err, &disabled)
userID := uint(0)
if disabled != nil {
userID = disabled.UserID
switch {
case errors.Is(err, biz.ErrAccountLocked):
var locked *biz.AccountLockedError
errors.As(err, &locked)
minutes := 0
if locked != nil {
minutes = locked.Minutes
}
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户被禁止登录", Agent: c.Request.UserAgent(), UserID: userID})
httpx.Fail(c, "账号已锁定,请 "+strconv.Itoa(minutes)+" 分钟后再试")
case errors.Is(err, biz.ErrCaptchaInvalid):
httpx.Fail(c, "验证码错误")
case errors.Is(err, biz.ErrUserDisabled):
httpx.Fail(c, "用户被禁止登录")
return
case errors.Is(err, biz.ErrTokenIssue):
httpx.Fail(c, "获取token失败")
case errors.Is(err, biz.ErrLoginState):
httpx.Fail(c, "设置登录状态失败")
case errors.Is(err, biz.ErrTokenRevoke):
httpx.Fail(c, "jwt作废失败")
default:
httpx.Fail(c, "用户名不存在或者密码错误")
}
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()})
if security != nil && security.LockEnable {
lockTTL := time.Duration(security.LockDuration) * time.Minute
failures, _ := h.settings.IncrementLoginFailure(c.Request.Context(), req.Username, lockTTL)
if int(failures) >= security.LockThreshold {
_ = h.settings.LockLogin(c.Request.Context(), req.Username, lockTTL)
}
}
httpx.Fail(c, "用户名不存在或者密码错误")
return
}
userID, _ := result.User["ID"].(uint)
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: true, Agent: c.Request.UserAgent(), UserID: userID})
h.settings.ClearLoginState(c.Request.Context(), req.Username)
maxAge := int(time.Until(time.UnixMilli(result.ExpiresAt)).Seconds())
httpx.SetTokenCookie(c, result.Token, maxAge)
httpx.Write(c, httpx.CodeSuccess, result, "登录成功")
@@ -165,28 +123,23 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
values = append(values, dto.Route{Path: route.Path, Method: route.Method})
}
if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil {
if errors.Is(err, biz.ErrTaskRuntimeReload) {
httpx.Fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error())
return
}
httpx.Fail(c, "自动创建数据库失败: "+err.Error())
return
}
if err := h.scheduler.Reload(c.Request.Context()); err != nil {
httpx.Fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "自动创建数据库成功")
}
}
type captchaStore struct {
service *service.SecurityService
runtime *conf.Runtime
}
func (s *captchaStore) Set(id, value string) error {
expiration := 3 * time.Minute
config := s.runtime.Admin()
if config != nil && config.Captcha != nil && config.Captcha.StoreExpiration != nil {
expiration = config.Captcha.StoreExpiration.AsDuration()
}
expiration := s.service.CaptchaSettings().StoreExpiration
return s.service.SetCaptcha(context.Background(), id, value, expiration)
}
func (s *captchaStore) Get(id string, clear bool) string {
+28
View File
@@ -0,0 +1,28 @@
package handler
type Set struct {
Authority *Authority
Menu *Menu
API *API
Permission *Permission
Organization *Organization
Announcement *Announcement
Email *Email
Task *Task
Media *Media
Audit *Audit
Export *Export
Version *Version
Dictionary *Dictionary
Parameter *Parameter
APIToken *APIToken
SystemConfig *SystemConfig
Public *Public
User *User
Navigation *Navigation
Session *Session
}
func NewSet(authority *Authority, menu *Menu, api *API, permission *Permission, organization *Organization, announcement *Announcement, email *Email, task *Task, media *Media, audit *Audit, export *Export, version *Version, dictionary *Dictionary, parameter *Parameter, apiToken *APIToken, systemConfig *SystemConfig, public *Public, user *User, navigation *Navigation, session *Session) *Set {
return &Set{Authority: authority, Menu: menu, API: api, Permission: permission, Organization: organization, Announcement: announcement, Email: email, Task: task, Media: media, Audit: audit, Export: export, Version: version, Dictionary: dictionary, Parameter: parameter, APIToken: apiToken, SystemConfig: systemConfig, Public: public, User: user, Navigation: navigation, Session: session}
}
+11 -10
View File
@@ -1,22 +1,23 @@
package handler
import (
"errors"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"kra/internal/worker"
"github.com/gin-gonic/gin"
)
type SystemConfig struct {
system *service.SystemConfigService
settings *service.SecurityService
scheduler *worker.TaskScheduler
system *service.SystemConfigService
settings *service.SecurityService
}
func NewSystemConfig(system *service.SystemConfigService, settings *service.SecurityService, scheduler *worker.TaskScheduler) *SystemConfig {
return &SystemConfig{system: system, settings: settings, scheduler: scheduler}
func NewSystemConfig(system *service.SystemConfigService, settings *service.SecurityService) *SystemConfig {
return &SystemConfig{system: system, settings: settings}
}
func (h *SystemConfig) GetSecurity(c *gin.Context) {
@@ -61,13 +62,13 @@ func (h *SystemConfig) Set(c *gin.Context) {
func (h *SystemConfig) Reload(c *gin.Context) {
if err := h.system.ReloadConfig(c.Request.Context()); err != nil {
if errors.Is(err, biz.ErrTaskRuntimeReload) {
httpx.Fail(c, "系统配置已重载,但定时任务重载失败:"+err.Error())
return
}
httpx.Fail(c, "重载系统失败:"+err.Error())
return
}
if err := h.scheduler.Reload(c.Request.Context()); err != nil {
httpx.Fail(c, "系统配置已重载,但定时任务重载失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "重载系统成功")
}
+22 -23
View File
@@ -1,26 +1,26 @@
package handler
import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
"kra/internal/biz"
"kra/internal/server/httpx"
"kra/internal/service"
"kra/internal/service/dto"
"kra/internal/worker"
"github.com/gin-gonic/gin"
)
type Task struct {
service *service.TaskService
scheduler *worker.TaskScheduler
service *service.TaskService
}
func NewTask(service *service.TaskService, scheduler *worker.TaskScheduler) *Task {
return &Task{service: service, scheduler: scheduler}
func NewTask(service *service.TaskService) *Task {
return &Task{service: service}
}
func (h *Task) Create(c *gin.Context) {
@@ -29,15 +29,17 @@ func (h *Task) Create(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
id, err := h.service.CreateRequest(c.Request.Context(), &req)
_, err := h.service.CreateRequest(c.Request.Context(), &req)
if err != nil {
if errors.Is(err, biz.ErrTaskSchedule) {
var scheduleErr *biz.TaskScheduleError
errors.As(err, &scheduleErr)
httpx.Fail(c, "调度失败:"+scheduleErr.Error())
return
}
httpx.Fail(c, "创建失败:"+err.Error())
return
}
if err = h.scheduler.ScheduleID(c.Request.Context(), id); err != nil {
httpx.Fail(c, "调度失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功")
}
@@ -48,13 +50,15 @@ func (h *Task) Update(c *gin.Context) {
return
}
if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil {
if errors.Is(err, biz.ErrTaskSchedule) {
var scheduleErr *biz.TaskScheduleError
errors.As(err, &scheduleErr)
httpx.Fail(c, "调度失败:"+scheduleErr.Error())
return
}
httpx.Fail(c, "更新失败:"+err.Error())
return
}
if err := h.scheduler.ScheduleID(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "调度失败:"+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功")
}
@@ -64,7 +68,6 @@ func (h *Task) Delete(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
h.scheduler.Remove(req.ID)
if err := h.service.Delete(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "删除失败")
return
@@ -82,10 +85,6 @@ func (h *Task) Toggle(c *gin.Context) {
httpx.Fail(c, "操作失败: "+err.Error())
return
}
if err := h.scheduler.ScheduleID(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "操作失败: "+err.Error())
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{}, "操作成功")
}
@@ -95,7 +94,7 @@ func (h *Task) Trigger(c *gin.Context) {
httpx.Fail(c, "参数错误")
return
}
if err := h.scheduler.TriggerID(c.Request.Context(), req.ID); err != nil {
if err := h.service.Trigger(c.Request.Context(), req.ID); err != nil {
httpx.Fail(c, "触发失败: "+err.Error())
return
}
@@ -110,7 +109,7 @@ func (h *Task) List(c *gin.Context) {
value, _ := strconv.ParseBool(raw)
enabled = &value
}
items, total, err := h.service.ListRequest(c.Request.Context(), page, size, c.Query("name"), c.Query("executorType"), enabled, h.scheduler.NextRuns())
items, total, err := h.service.ListRequest(c.Request.Context(), page, size, c.Query("name"), c.Query("executorType"), enabled)
if err != nil {
httpx.Fail(c, "获取失败")
return
@@ -146,8 +145,8 @@ func (h *Task) AlertStream(c *gin.Context) {
}
// Remove the normal server deadline for long-lived SSE connections.
_ = http.NewResponseController(c.Writer).SetWriteDeadline(time.Time{})
events := h.scheduler.Subscribe()
defer h.scheduler.Unsubscribe(events)
events := h.service.Subscribe()
defer h.service.Unsubscribe(events)
ticker := time.NewTicker(20 * time.Second)
defer ticker.Stop()
_, _ = fmt.Fprint(c.Writer, "event: connected\ndata: {}\n\n")