Files
kra-oa/internal/server/handler/public.go
T

201 lines
7.2 KiB
Go

package handler
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"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 {
system *service.SystemService
settings *service.SettingsService
audit *service.AuditService
scheduler *worker.TaskScheduler
store *captchaStore
keyLong int
imageWidth int
imageHeight int
}
func NewPublic(config *conf.AdminBackend, system *service.SystemService, settings *service.SettingsService, audit *service.AuditService, scheduler *worker.TaskScheduler) *Public {
expiration := 3 * time.Minute
keyLong, width, height := 6, 240, 80
if config != nil && config.Captcha != nil {
if config.Captcha.StoreExpiration != nil {
expiration = config.Captcha.StoreExpiration.AsDuration()
}
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 &Public{system: system, settings: settings, audit: audit, scheduler: scheduler, store: &captchaStore{service: system, expiration: expiration}, keyLong: keyLong, imageWidth: width, imageHeight: height}
}
func (h *Public) Captcha(c *gin.Context) {
keyLong, width, height := h.keyLong, h.imageWidth, h.imageHeight
security, _ := h.settings.CurrentSecurity(c.Request.Context())
openCaptcha := true
if security != nil {
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
if security.CaptchaOpen > 0 {
failures, _ := cachedInt(c.Request.Context(), h.system, "login:fail:"+c.ClientIP())
openCaptcha = failures >= security.CaptchaOpen
}
}
if !openCaptcha {
httpx.Write(c, httpx.CodeSuccess, gin.H{"captchaId": "", "picPath": "", "captchaLength": keyLong, "openCaptcha": false}, "验证码获取成功")
return
}
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
id, picture, _, err := base64Captcha.NewCaptcha(driver, h.store).Generate()
if err != nil {
httpx.Fail(c, "验证码获取失败")
return
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"captchaId": id, "picPath": picture, "captchaLength": keyLong, "openCaptcha": openCaptcha}, "验证码获取成功")
}
func (h *Public) Login(c *gin.Context) {
var req dto.LoginRequest
if c.ShouldBindJSON(&req) != nil || req.Username == "" || req.Password == "" {
httpx.Fail(c, "请输入用户名和密码")
return
}
security, _ := h.settings.CurrentSecurity(c.Request.Context())
if security != nil && security.LockEnable {
if _, locked, _ := h.system.CacheGet(c.Request.Context(), "login:lock:"+req.Username); locked {
httpx.Fail(c, "账户已锁定,请稍后重试")
return
}
}
requireCaptcha := security == nil || security.CaptchaOpen == 0
if security != nil && security.CaptchaOpen > 0 {
failures, _ := cachedInt(c.Request.Context(), h.system, "login:fail:"+c.ClientIP())
requireCaptcha = failures >= security.CaptchaOpen
}
if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !h.store.Verify(req.CaptchaID, req.Captcha, true)) {
httpx.Fail(c, "验证码错误")
return
}
result, err := h.system.Login(c.Request.Context(), req.Username, req.Password)
if err != nil {
_ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()})
ttl := time.Hour
if security != nil && security.CaptchaTimeout > 0 {
ttl = time.Duration(security.CaptchaTimeout) * time.Second
}
failures, _ := incrementCached(c.Request.Context(), h.system, "login:fail:"+c.ClientIP(), ttl)
if security != nil && security.LockEnable && failures >= security.LockThreshold {
_ = h.system.CacheSet(c.Request.Context(), "login:lock:"+req.Username, "1", time.Duration(security.LockDuration)*time.Minute)
}
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.system.CacheDelete(c.Request.Context(), "login:fail:"+c.ClientIP())
_ = h.system.CacheDelete(c.Request.Context(), "login:lock:"+req.Username)
maxAge := int(time.Until(time.UnixMilli(result.ExpiresAt)).Seconds())
http.SetCookie(c.Writer, &http.Cookie{Name: "x-token", Value: result.Token, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteStrictMode})
httpx.Write(c, httpx.CodeSuccess, result, "登录成功")
}
func (h *Public) CheckDatabase(c *gin.Context) {
initialized, err := h.system.IsInitialized(c.Request.Context())
if err != nil {
httpx.Fail(c, "数据库状态检查失败")
return
}
message := "数据库无需初始化"
if !initialized {
message = "前往初始化数据库"
}
httpx.Write(c, httpx.CodeSuccess, gin.H{"needInit": !initialized}, message)
}
func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
initialized, err := h.system.IsInitialized(c.Request.Context())
if err != nil {
httpx.Fail(c, "数据库状态检查失败")
return
}
if initialized {
httpx.Fail(c, "数据库已初始化,无需重复初始化")
return
}
var input service.DatabaseInit
if c.ShouldBindJSON(&input) != nil {
httpx.Fail(c, "数据库初始化参数无效")
return
}
routes := engine.Routes()
values := make([]dto.Route, 0, len(routes))
for _, route := range routes {
values = append(values, dto.Route{Path: route.Path, Method: route.Method})
}
if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil {
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{}, "自动创建数据库成功")
}
}
func cachedInt(ctx context.Context, system *service.SystemService, key string) (int, error) {
value, ok, err := system.CacheGet(ctx, key)
if err != nil || !ok {
return 0, err
}
return strconv.Atoi(value)
}
func incrementCached(ctx context.Context, system *service.SystemService, key string, ttl time.Duration) (int, error) {
value, err := system.CacheIncrement(ctx, key, ttl)
return int(value), err
}
type captchaStore struct {
service *service.SystemService
expiration time.Duration
}
func (s *captchaStore) key(id string) string { return "captcha:" + id }
func (s *captchaStore) Set(id, value string) error {
return s.service.CacheSet(context.Background(), s.key(id), value, s.expiration)
}
func (s *captchaStore) Get(id string, clear bool) string {
value, ok, err := s.service.CacheGet(context.Background(), s.key(id))
if err != nil || !ok {
return ""
}
if clear {
_ = s.service.CacheDelete(context.Background(), s.key(id))
}
return value
}
func (s *captchaStore) Verify(id, answer string, clear bool) bool {
return id != "" && answer != "" && strings.EqualFold(s.Get(id, clear), answer)
}