package middleware import ( "crypto/sha256" "encoding/hex" "net/http" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "nl-pms-api/internal/commonservice" "nl-pms-api/internal/model" ) const ( HeaderAdminStepUp = "X-Admin-StepUp" CtxClientIP = "client_ip" ) // AttachClientIP 把解析后的客户端 IP 写入上下文。 func AttachClientIP() gin.HandlerFunc { return func(c *gin.Context) { c.Set(CtxClientIP, strings.TrimSpace(c.ClientIP())) c.Next() } } // ClientIP 读取中间件写入的客户端 IP。 func ClientIP(c *gin.Context) string { v, _ := c.Get(CtxClientIP) s, _ := v.(string) if s == "" { return strings.TrimSpace(c.ClientIP()) } return s } // RejectDisabled 拒绝已禁用账号(在 JWT 之后)。 func RejectDisabled(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { uid := commonservice.UserID(c) if uid <= 0 { c.Next() return } var u model.User if err := db.Select("id", "disabled").First(&u, uid).Error; err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "UNAUTHORIZED"}) return } if u.Disabled != 0 { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ACCOUNT_DISABLED"}) return } c.Next() } } // RequireAdmin 仅允许 users.id=1。 func RequireAdmin() gin.HandlerFunc { return func(c *gin.Context) { if commonservice.UserID(c) != commonservice.AdminUserID { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "FORBIDDEN"}) return } c.Next() } } // RequireAdminStepUp 校验 2 小时内有效的动态码二次验证,且 IP 与签发时一致。 func RequireAdminStepUp(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { if commonservice.UserID(c) != commonservice.AdminUserID { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "FORBIDDEN"}) return } token := strings.TrimSpace(c.GetHeader(HeaderAdminStepUp)) if token == "" { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ADMIN_STEPUP_REQUIRED"}) return } ip := ClientIP(c) var row model.AdminStepup if err := db.First(&row, commonservice.AdminUserID).Error; err != nil { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ADMIN_STEPUP_REQUIRED"}) return } sum := sha256.Sum256([]byte(token)) hash := hex.EncodeToString(sum[:]) if row.TokenHash != hash { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ADMIN_STEPUP_REQUIRED"}) return } exp, err := time.Parse(time.RFC3339, row.ExpiresAt) if err != nil || time.Now().UTC().After(exp) { _ = db.Delete(&model.AdminStepup{}, commonservice.AdminUserID).Error c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ADMIN_STEPUP_REQUIRED"}) return } if row.ClientIP != "" && ip != "" && row.ClientIP != ip { _ = db.Delete(&model.AdminStepup{}, commonservice.AdminUserID).Error c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ "error": "ADMIN_IP_CHANGED", "message": "检测到 IP 变化,存在账号被盗风险,请重新输入动态码", }) return } c.Next() } } // RequireStepUpForGlobalSetting 仅当写入全局设置键时要求 stepup。 func RequireStepUpForGlobalSetting(db *gorm.DB) gin.HandlerFunc { inner := RequireAdminStepUp(db) return func(c *gin.Context) { name := strings.TrimSpace(c.Param("name")) if name == "file_storage" || strings.HasPrefix(name, "fest_img:") { inner(c) return } c.Next() } }