package auth import ( "finance/backend/internal/config" "finance/backend/internal/database" "finance/backend/internal/models" "fmt" "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) // Request and Response Types // SignupRequest represents the data needed for registering a new user type SignupRequest struct { Name string `json:"name" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=8"` } // LoginRequest represents the data needed for user login type LoginRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` } // AuthResponse is returned on successful authentication type AuthResponse struct { Token string `json:"token"` User models.User `json:"user"` } // JWT Token Functions // GenerateJWT creates a new JWT token for a given user ID. func GenerateJWT(userID uint, cfg *config.Config) (string, error) { // Create the claims claims := jwt.MapClaims{ "sub": userID, // Subject (user ID) "iss": "finance-app", // Issuer "aud": "finance-app-users", // Audience "exp": time.Now().Add(time.Hour * 24).Unix(), // Expiration time (e.g., 24 hours) "iat": time.Now().Unix(), // Issued at } // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign token with secret signedToken, err := token.SignedString([]byte(cfg.JWTSecret)) if err != nil { return "", fmt.Errorf("failed to sign token: %w", err) } return signedToken, nil } // ValidateJWT validates a JWT token and extracts the user ID from it. // Returns the user ID if the token is valid, or an error if it's not. func ValidateJWT(tokenString string, cfg *config.Config) (uint, error) { // Parse the token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Validate the signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } // Return the secret key return []byte(cfg.JWTSecret), nil }) if err != nil { return 0, fmt.Errorf("failed to parse token: %w", err) } // Check if token is valid if !token.Valid { return 0, fmt.Errorf("invalid token") } // Extract claims claims, ok := token.Claims.(jwt.MapClaims) if !ok { return 0, fmt.Errorf("invalid token claims") } // Verify expiration time if exp, ok := claims["exp"].(float64); ok { if time.Now().Unix() > int64(exp) { return 0, fmt.Errorf("token expired") } } else { return 0, fmt.Errorf("invalid expiration claim") } // Extract user ID from subject claim sub, ok := claims["sub"].(float64) if !ok { return 0, fmt.Errorf("invalid subject claim") } // Convert to uint and return return uint(sub), nil } // Handler Functions // Signup handles user registration func Signup(cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { var req SignupRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Check if user with this email already exists var existingUser models.User result := database.DB.Where("email = ?", req.Email).First(&existingUser) if result.RowsAffected > 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "User with this email already exists"}) return } // Create new user user := models.User{ Name: req.Name, Email: req.Email, } // Set password (this will hash it) if err := user.SetPassword(req.Password); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process password"}) return } // Save user to database if err := database.DB.Create(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) return } // Generate JWT token token, err := GenerateJWT(user.ID, cfg) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate authentication token"}) return } // Clear password hash before returning user data user.PasswordHash = "" c.JSON(http.StatusCreated, AuthResponse{ Token: token, User: user, }) } } // Login handles user authentication func Login(cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Find user by email var user models.User result := database.DB.Where("email = ?", req.Email).First(&user) if result.Error != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"}) return } // Check password if err := user.CheckPassword(req.Password); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"}) return } // Generate JWT token token, err := GenerateJWT(user.ID, cfg) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate authentication token"}) return } // Clear password hash before returning user data user.PasswordHash = "" c.JSON(http.StatusOK, AuthResponse{ Token: token, User: user, }) } } // AuthMiddleware validates JWT tokens and adds user data to the context func AuthMiddleware(cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) c.Abort() return } // Check if the Authorization header has the Bearer prefix 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 } // Extract the token tokenString := parts[1] // Validate the token userID, err := ValidateJWT(tokenString, cfg) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) c.Abort() return } // Find the user in the database var user models.User if err := database.DB.First(&user, userID).Error; err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"}) c.Abort() return } // Set user data in context c.Set("userID", userID) c.Set("user", user) c.Next() } }