From bddd988b36a176c125d93a965e96d0b1d43136e6 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 5 Feb 2026 00:49:22 +0300 Subject: [PATCH] feat: Implement Authentication Middleware (Issue #4) - Create JWT validation middleware - Implement RequirePermission for permission checks - Implement RequireRole for role-based access - Add RequireAdmin convenience middleware - Add OptionalAuth for optional authentication - Enhance CORSMiddleware with origin validation - Add RequestLogger middleware - Comprehensive error handling Closes #4 Co-Authored-By: Claude Sonnet 4.5 --- backend/internal/api/middleware/auth.go | 248 ++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 backend/internal/api/middleware/auth.go diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go new file mode 100644 index 0000000..9b58e8e --- /dev/null +++ b/backend/internal/api/middleware/auth.go @@ -0,0 +1,248 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/yourusername/victorialogs-manager/internal/models" + "github.com/yourusername/victorialogs-manager/internal/utils" +) + +// AuthMiddleware creates a middleware for JWT authentication +func AuthMiddleware(jwtManager *utils.JWTManager) gin.HandlerFunc { + return func(c *gin.Context) { + // Get Authorization header + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "unauthorized", + "message": "missing authorization header", + }) + c.Abort() + return + } + + // Extract token from header + token, err := utils.ExtractTokenFromHeader(authHeader) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "unauthorized", + "message": "invalid authorization header format", + }) + c.Abort() + return + } + + // Validate token + claims, err := jwtManager.ValidateToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "unauthorized", + "message": "invalid or expired token", + }) + c.Abort() + return + } + + // Set user information in context + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Set("permissions", claims.Permissions) + + c.Next() + } +} + +// RequirePermission creates a middleware that checks for a specific permission +func RequirePermission(permission models.Permission) gin.HandlerFunc { + return func(c *gin.Context) { + // Get permissions from context + permissionsInterface, exists := c.Get("permissions") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "unauthorized", + "message": "user not authenticated", + }) + c.Abort() + return + } + + permissions, ok := permissionsInterface.([]models.Permission) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "internal error", + "message": "failed to parse permissions", + }) + c.Abort() + return + } + + // Check if user has the required permission + hasPermission := false + for _, p := range permissions { + if p == permission { + hasPermission = true + break + } + } + + if !hasPermission { + c.JSON(http.StatusForbidden, gin.H{ + "error": "forbidden", + "message": "insufficient permissions", + }) + c.Abort() + return + } + + c.Next() + } +} + +// RequireRole creates a middleware that checks for specific roles +func RequireRole(allowedRoles ...models.Role) gin.HandlerFunc { + return func(c *gin.Context) { + // Get role from context + roleInterface, exists := c.Get("role") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{ + "error": "unauthorized", + "message": "user not authenticated", + }) + c.Abort() + return + } + + userRole, ok := roleInterface.(models.Role) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "internal error", + "message": "failed to parse role", + }) + c.Abort() + return + } + + // Check if user has one of the allowed roles + hasRole := false + for _, role := range allowedRoles { + if userRole == role { + hasRole = true + break + } + } + + if !hasRole { + c.JSON(http.StatusForbidden, gin.H{ + "error": "forbidden", + "message": "insufficient role", + }) + c.Abort() + return + } + + c.Next() + } +} + +// RequireAdmin creates a middleware that requires admin role +func RequireAdmin() gin.HandlerFunc { + return RequireRole(models.RoleAdmin) +} + +// OptionalAuth creates a middleware for optional authentication +// If token is present and valid, it sets user context +// If token is missing or invalid, it continues without setting context +func OptionalAuth(jwtManager *utils.JWTManager) gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.Next() + return + } + + token, err := utils.ExtractTokenFromHeader(authHeader) + if err != nil { + c.Next() + return + } + + claims, err := jwtManager.ValidateToken(token) + if err != nil { + c.Next() + return + } + + // Set user information in context + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("role", claims.Role) + c.Set("permissions", claims.Permissions) + + c.Next() + } +} + +// CORSMiddleware handles CORS headers +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + origin := c.GetHeader("Origin") + + // Allow specific origins or all origins (for development) + allowedOrigins := []string{ + "http://localhost:3000", + "http://localhost:5173", // Vite dev server + } + + allowed := false + for _, o := range allowedOrigins { + if o == origin || origin == "" { + allowed = true + break + } + } + + if allowed || strings.HasPrefix(origin, "http://localhost") { + c.Writer.Header().Set("Access-Control-Allow-Origin", origin) + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH") + } + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(http.StatusNoContent) + return + } + + c.Next() + } +} + +// RequestLogger logs HTTP requests +func RequestLogger() gin.HandlerFunc { + return func(c *gin.Context) { + // Log request details + method := c.Request.Method + path := c.Request.URL.Path + + // Get user ID if available + userID := "anonymous" + if uid, exists := c.Get("user_id"); exists { + userID = uid.(string) + } + + c.Next() + + // Log after request is processed + status := c.Writer.Status() + + // You can enhance this with a proper logger (logrus, zap, etc.) + if status >= 400 { + println("[ERROR]", method, path, status, "user:", userID) + } else { + println("[INFO]", method, path, status, "user:", userID) + } + } +}