package handlers import ( "context" "crypto/rand" "encoding/base64" "fmt" "log" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "github.com/starfleetcptn/gomft/components" "github.com/starfleetcptn/gomft/internal/auth" "github.com/starfleetcptn/gomft/internal/db" "golang.org/x/crypto/bcrypt" ) // Define a custom type for context keys to avoid string collisions type contextKey string // Context keys const ( themeKey contextKey = "theme" emailKey contextKey = "email" ) // AuthMiddleware is a middleware function that checks if the user is authenticated func (h *Handlers) AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Get the JWT token from the cookie tokenString, err := c.Cookie("jwt_token") if err != nil { c.Redirect(http.StatusFound, "/login") c.Abort() return } // Parse and validate the token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(h.JWTSecret), nil }) if err != nil || !token.Valid { c.Redirect(http.StatusFound, "/login") c.Abort() return } // Extract claims claims, ok := token.Claims.(jwt.MapClaims) if !ok { c.Redirect(http.StatusFound, "/login") c.Abort() return } // Safely extract claims with type assertions and defaults userID, ok := claims["user_id"].(float64) if !ok || userID <= 0 { c.Redirect(http.StatusFound, "/login") c.Abort() return } email, _ := claims["email"].(string) username, _ := claims["username"].(string) isAdmin, _ := claims["is_admin"].(bool) // Set user information in the context c.Set("userID", uint(userID)) if email != "" { c.Set("email", email) } if username != "" { c.Set("username", username) } c.Set("isAdmin", isAdmin) // Load user's roles and permissions var user db.User if h.DB != nil && userID > 0 { if err := h.DB.Preload("Roles").First(&user, uint(userID)).Error; err != nil { log.Printf("Error loading user roles: %v", err) // Continue without roles if there's an error } else { c.Set("user", &user) } } c.Next() } } // AdminMiddleware is a middleware function that checks if the user is an admin func (h *Handlers) AdminMiddleware() gin.HandlerFunc { return func(c *gin.Context) { isAdmin, exists := c.Get("isAdmin") if !exists || !isAdmin.(bool) { c.Redirect(http.StatusFound, "/dashboard") c.Abort() return } c.Next() } } // PermissionMiddleware creates a middleware that checks for specific permissions func (h *Handlers) PermissionMiddleware(requiredPermissions ...string) gin.HandlerFunc { return func(c *gin.Context) { // Get user from context user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found in context"}) c.Abort() return } // Type assert to *db.User u, ok := user.(*db.User) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user type in context"}) c.Abort() return } // Check if user is admin (admins have all permissions) isAdmin, exists := c.Get("isAdmin") if exists && isAdmin.(bool) { c.Next() return } // Check each required permission for _, permission := range requiredPermissions { if !u.HasPermission(permission) { // If it's an API request, return JSON if strings.HasPrefix(c.Request.URL.Path, "/api/") { c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) } else { // For web requests, redirect to dashboard with error message c.Redirect(http.StatusFound, "/dashboard?error=insufficient_permissions") } c.Abort() return } } c.Next() } } // APIAuthMiddleware is a middleware function that checks if the API request is authenticated func (h *Handlers) APIAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // Get the Authorization header authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) c.Abort() return } // Check if the header is in the correct format parts := strings.Split(authHeader, " ") if len(parts) != 2 || parts[0] != "Bearer" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) c.Abort() return } // Parse and validate the token tokenString := parts[1] token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(h.JWTSecret), nil }) if err != nil || !token.Valid { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) c.Abort() return } // Extract claims claims, ok := token.Claims.(jwt.MapClaims) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"}) c.Abort() return } // Safely extract user ID userIDFloat, ok := claims["user_id"].(float64) if !ok || userIDFloat <= 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID in token"}) c.Abort() return } userID := uint(userIDFloat) c.Set("userID", userID) // Extract other claims if email, ok := claims["email"].(string); ok { c.Set("email", email) } if username, ok := claims["username"].(string); ok { c.Set("username", username) } if isAdmin, ok := claims["is_admin"].(bool); ok { c.Set("isAdmin", isAdmin) } // Load user's roles and permissions for API requests if h.DB != nil { var user db.User if err := h.DB.Preload("Roles").First(&user, userID).Error; err != nil { log.Printf("Error loading user roles: %v", err) // Continue without roles } else { c.Set("user", &user) } } c.Next() } } // APIAdminMiddleware is a middleware function that checks if the API request is from an admin func (h *Handlers) APIAdminMiddleware() gin.HandlerFunc { return func(c *gin.Context) { isAdmin, exists := c.Get("isAdmin") if !exists || !isAdmin.(bool) { c.JSON(http.StatusForbidden, gin.H{"error": "Admin privileges required"}) c.Abort() return } c.Next() } } // GenerateJWT generates a JWT token for the given user func (h *Handlers) GenerateJWT(userID uint, email string, isAdmin bool) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "user_id": userID, "email": email, "username": strings.Split(email, "@")[0], // Use email prefix as username "is_admin": isAdmin, "exp": time.Now().Add(time.Hour * 24).Unix(), }) return token.SignedString([]byte(h.JWTSecret)) } // HandleLoginPage handles the GET /login route func (h *Handlers) HandleLoginPage(c *gin.Context) { // Check if user is already logged in if userID, exists := c.Get("userID"); exists && userID != nil { // User is logged in, redirect to dashboard c.Redirect(http.StatusFound, "/dashboard") return } // Create template context and set email if available ctx := components.CreateTemplateContext(c) if email, exists := c.Get("email"); exists { ctx = context.WithValue(ctx, emailKey, email) } // Check for message query param (used for password expired, etc.) message := c.Query("message") // User is not logged in, show login page if message != "" { components.Login(ctx, message).Render(c.Request.Context(), c.Writer) } else { components.Login(ctx, "").Render(c.Request.Context(), c.Writer) } } // HandleLogin handles the POST /login route func (h *Handlers) HandleLogin(c *gin.Context) { email := c.PostForm("email") password := c.PostForm("password") // Get user by email var user db.User if err := h.DB.Where("email = ?", email).First(&user).Error; err != nil { components.Login(components.CreateTemplateContext(c), "Invalid credentials").Render(c, c.Writer) return } // Check if account is locked if user.GetAccountLocked() { if user.LockoutUntil != nil && time.Now().After(*user.LockoutUntil) { // Lockout period has expired, reset the lockout user.SetAccountLocked(false) user.FailedLoginAttempts = 0 user.LockoutUntil = nil h.DB.Save(&user) } else { // Account is still locked components.Login(components.CreateTemplateContext(c), "Account is locked due to too many failed login attempts. Please try again later.").Render(c, c.Writer) return } } // Check password if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil { // Increment failed login attempts user.FailedLoginAttempts++ // Check if we need to lock the account policy := auth.DefaultPasswordPolicy() if user.FailedLoginAttempts >= policy.MaxLoginAttempts { user.SetAccountLocked(true) lockoutTime := time.Now().Add(policy.LockoutDuration) user.LockoutUntil = &lockoutTime h.DB.Save(&user) components.Login(components.CreateTemplateContext(c), "Account is locked due to too many failed login attempts. Please try again later.").Render(c, c.Writer) return } h.DB.Save(&user) components.Login(components.CreateTemplateContext(c), "Invalid credentials").Render(c, c.Writer) return } // Reset failed login attempts on successful login user.FailedLoginAttempts = 0 user.SetAccountLocked(false) user.LockoutUntil = nil h.DB.Save(&user) // Check password expiration policy := auth.DefaultPasswordPolicy() if auth.IsPasswordExpired(user.LastPasswordChange, policy) { // Add flash message about password expiration // We're simplifying by just redirecting to login with a message c.SetCookie("jwt_token", "", -1, "/", "", false, true) // Logout the user c.Redirect(http.StatusFound, "/login?message=Your+password+has+expired.+Please+contact+an+administrator.") return } // Check if 2FA is enabled if user.TwoFactorEnabled { // Store user ID temporarily for 2FA verification c.SetCookie("temp_user_id", fmt.Sprintf("%d", user.ID), 300, "/", "", false, true) // 5 minutes expiry // Redirect to 2FA verification page c.Redirect(http.StatusFound, "/login/verify") return } // If 2FA is not enabled, proceed with normal login // Generate JWT token with all necessary user information isAdmin := false if user.IsAdmin != nil { isAdmin = *user.IsAdmin } token, err := h.GenerateJWT(user.ID, user.Email, isAdmin) if err != nil { components.Login(components.CreateTemplateContext(c), "Authentication error").Render(c, c.Writer) return } // Set token in cookie c.SetCookie("jwt_token", token, 86400, "/", "", false, true) c.Redirect(http.StatusFound, "/dashboard") } // HandleLogout handles the POST /logout route func (h *Handlers) HandleLogout(c *gin.Context) { c.SetCookie("jwt_token", "", -1, "/", "", false, true) c.Redirect(http.StatusFound, "/login") } // HandleChangePassword handles the POST /change-password route // This is now only for use from the profile page func (h *Handlers) HandleChangePassword(c *gin.Context) { // Get user ID from token tokenCookie, err := c.Cookie("jwt_token") if err != nil || tokenCookie == "" { if c.GetHeader("HX-Request") == "true" { c.Data(http.StatusUnauthorized, "text/html", []byte(`