aboutsummaryrefslogtreecommitdiffstats
path: root/backend/internal/api/auth/auth.go
diff options
context:
space:
mode:
authorLibravatarLibravatar Biswa Kalyan Bhuyan <biswa@surgot.in> 2025-04-24 08:18:27 +0530
committerLibravatarLibravatar Biswa Kalyan Bhuyan <biswa@surgot.in> 2025-04-24 08:18:27 +0530
commit50d5e6534f5e593297a09323e683c7c8b850117b (patch)
tree339d6e8b123c5d4caa4129971e2cb1b960b12a89 /backend/internal/api/auth/auth.go
parent76066679b5bdab53419492066c4e80d2ed3be518 (diff)
downloadfinance-50d5e6534f5e593297a09323e683c7c8b850117b.tar.gz
finance-50d5e6534f5e593297a09323e683c7c8b850117b.tar.bz2
finance-50d5e6534f5e593297a09323e683c7c8b850117b.zip
feat: added basic backend features to it
- Set up API framework (Gin Gonic) - Set up ORM/DB library (GORM) - Design database schema (Users, Accounts, Transactions, Loans, Goals) - Set up database connection and migrations
Diffstat (limited to 'backend/internal/api/auth/auth.go')
-rw-r--r--backend/internal/api/auth/auth.go247
1 files changed, 247 insertions, 0 deletions
diff --git a/backend/internal/api/auth/auth.go b/backend/internal/api/auth/auth.go
new file mode 100644
index 0000000..2f8fa6a
--- /dev/null
+++ b/backend/internal/api/auth/auth.go
@@ -0,0 +1,247 @@
+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()
+ }
+}