This commit is contained in:
Tomas Dvorak
2025-11-11 10:29:30 +01:00
parent d5b4faea61
commit 8762bde4bf
139 changed files with 7240 additions and 2870 deletions
+49
View File
@@ -75,6 +75,55 @@ func JWTAuth(db *gorm.DB) gin.HandlerFunc {
}
}
// JWTOptional attempts to authenticate the request if a token or auth cookie is present.
func JWTOptional(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if config.AppConfig != nil && config.AppConfig.AppEnv != "production" {
if strings.ToLower(c.GetHeader("X-Dev-Admin")) == "true" {
c.Set("userRole", "admin")
c.Set("user", &models.User{Role: "admin"})
c.Next()
return
}
}
var tokenString string
if authHeader := c.GetHeader("Authorization"); authHeader != "" {
parts := strings.Split(authHeader, " ")
if len(parts) == 2 && parts[0] == "Bearer" {
tokenString = parts[1]
}
}
if tokenString == "" {
if cookie, err := c.Request.Cookie("auth_token"); err == nil {
tokenString = cookie.Value
}
}
if tokenString == "" {
c.Next()
return
}
claims, err := utils.ParseJWT(tokenString)
if err != nil {
c.Next()
return
}
var user models.User
if err := db.First(&user, claims.UserID).Error; err != nil {
c.Next()
return
}
c.Set("user", &user)
c.Set("claims", claims)
c.Set("userID", user.ID)
c.Set("userRole", user.Role)
c.Next()
}
}
// DevBypass checks for special dev header and grants admin role when not in production
func DevBypass() gin.HandlerFunc {
return func(c *gin.Context) {
+9
View File
@@ -7,6 +7,7 @@ import (
"sync"
"time"
"fotbal-club/internal/config"
"github.com/gin-gonic/gin"
)
@@ -64,6 +65,14 @@ func CSRFProtection() gin.HandlerFunc {
return
}
// Dev-only: skip CSRF when using X-Admin-Token (remote admin tools)
if config.AppConfig != nil && config.AppConfig.AppEnv != "production" {
if token := c.GetHeader("X-Admin-Token"); token != "" && token == config.AppConfig.AdminAccessToken {
c.Next()
return
}
}
// Get token from header or form
token := c.GetHeader("X-CSRF-Token")
if token == "" {
+2 -2
View File
@@ -15,10 +15,10 @@ func DBContext() gin.HandlerFunc {
// 15 seconds is generous for most queries while preventing indefinite hangs
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
// Store the context so controllers can use it with db.WithContext(ctx)
c.Set("dbCtx", ctx)
c.Next()
}
}
@@ -0,0 +1,44 @@
package middleware
import (
"strings"
"fotbal-club/internal/services"
"fotbal-club/pkg/logger"
"github.com/gin-gonic/gin"
)
func ErrorStatusReporter(reporter *services.ErrorReporter) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if reporter == nil {
return
}
status := c.Writer.Status()
if status >= 500 {
msg := ""
if len(c.Errors) > 0 {
var parts []string
for _, e := range c.Errors {
if e != nil && e.Err != nil {
parts = append(parts, e.Err.Error())
} else if e != nil {
parts = append(parts, e.Error())
}
}
msg = strings.Join(parts, "; ")
}
reporter.Report(c.Request.Context(), &services.ErrorEvent{
Origin: "backend",
Language: "go",
Severity: "error",
Message: msg,
URL: c.Request.URL.Path,
Method: c.Request.Method,
Status: status,
RequestID: GetRequestID(c),
})
logger.Error("Reported 5xx status=%d path=%s", status, c.Request.URL.Path)
}
}
}
+38
View File
@@ -6,6 +6,7 @@ import (
"runtime/debug"
"fotbal-club/pkg/logger"
"fotbal-club/internal/services"
"github.com/gin-gonic/gin"
)
@@ -41,3 +42,40 @@ func CustomRecovery() gin.HandlerFunc {
c.Next()
}
}
func CustomRecoveryWithReporter(reporter *services.ErrorReporter) gin.HandlerFunc {
if reporter == nil {
return CustomRecovery()
}
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
stack := string(debug.Stack())
requestID := GetRequestID(c)
logger.Error("Panic recovered",
"request_id", requestID,
"error", fmt.Sprintf("%v", err),
"stack", stack,
"path", c.Request.URL.Path,
"method", c.Request.Method,
)
reporter.Report(c.Request.Context(), &services.ErrorEvent{
Origin: "backend",
Language: "go",
Severity: "fatal",
Message: fmt.Sprintf("%v", err),
Stack: stack,
URL: c.Request.URL.Path,
Method: c.Request.Method,
RequestID: requestID,
})
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error",
"request_id": requestID,
})
c.Abort()
}
}()
c.Next()
}
}
+12 -1
View File
@@ -41,7 +41,18 @@ func ValidateContentType() gin.HandlerFunc {
path := c.Request.URL.Path
// Allow multipart for uploads and image processing crop upload
if strings.Contains(path, "/upload") || strings.Contains(path, "/image-processing/crop-upload") {
if strings.Contains(path, "/upload") || strings.Contains(path, "/image-processing/crop-upload") || strings.Contains(path, "/admin/scoreboard/qr") || strings.Contains(path, "/admin/scoreboard/load") {
c.Next()
return
}
// Allow scoreboard timer control endpoints without requiring JSON body
// These actions do not read request body and are triggered via simple POSTs from remote UI
if strings.Contains(path, "/admin/scoreboard/timer/") {
c.Next()
return
}
if strings.Contains(path, "/admin/scoreboard/swap-sides") || strings.Contains(path, "/admin/scoreboard/second-half") {
c.Next()
return
}