package api import ( "containr/internal/database" "database/sql" "net/http" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) type LoginRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` } type RegisterRequest struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` Name string `json:"name" binding:"required,min=2"` } type AuthResponse struct { Token string `json:"token"` User interface{} `json:"user"` } type User struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` AvatarURL string `json:"avatar_url,omitempty"` CreatedAt string `json:"created_at"` } func handleLogin(c *gin.Context) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } db := c.MustGet("db").(*database.DB) jwtSecret := c.MustGet("jwt_secret").(string) // Find user by email var user User var hashedPassword string err := db.QueryRow(` SELECT id, email, password_hash, name, COALESCE(avatar_url, ''), created_at FROM users WHERE email = $1 `, req.Email).Scan(&user.ID, &user.Email, &hashedPassword, &user.Name, &user.AvatarURL, &user.CreatedAt) if err == sql.ErrNoRows { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } // Check password if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) return } // Generate JWT token token, err := generateJWT(user.ID, user.Email, jwtSecret) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } c.JSON(http.StatusOK, AuthResponse{ Token: token, User: user, }) } func handleRegister(c *gin.Context) { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } db := c.MustGet("db").(*database.DB) jwtSecret := c.MustGet("jwt_secret").(string) // Check if user already exists var count int err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", req.Email).Scan(&count) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } if count > 0 { c.JSON(http.StatusConflict, gin.H{"error": "User already exists"}) return } // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) return } // Create user var user User err = db.QueryRow(` INSERT INTO users (email, password_hash, name) VALUES ($1, $2, $3) RETURNING id, email, name, COALESCE(avatar_url, ''), created_at `, req.Email, string(hashedPassword), req.Name).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.CreatedAt) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) return } // Generate JWT token token, err := generateJWT(user.ID, user.Email, jwtSecret) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } c.JSON(http.StatusCreated, AuthResponse{ Token: token, User: user, }) } func handleGetProfile(c *gin.Context) { userID := c.MustGet("user_id").(string) db := c.MustGet("db").(*database.DB) var user User err := db.QueryRow(` SELECT id, email, name, COALESCE(avatar_url, ''), created_at FROM users WHERE id = $1 `, userID).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.CreatedAt) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"}) return } c.JSON(http.StatusOK, user) } func handleUpdateProfile(c *gin.Context) { userID := c.MustGet("user_id").(string) db := c.MustGet("db").(*database.DB) var req struct { Name string `json:"name,omitempty"` AvatarURL string `json:"avatar_url,omitempty"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Update user profile _, err := db.Exec(` UPDATE users SET name = COALESCE($1, name), avatar_url = COALESCE($2, avatar_url) WHERE id = $3 `, req.Name, req.AvatarURL, userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"}) return } // Return updated user handleGetProfile(c) } func generateJWT(userID, email, secret string) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "user_id": userID, "email": email, "exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7 days }) return token.SignedString([]byte(secret)) } // ValidateJWT validates a JWT token and returns the claims func ValidateJWT(tokenString, secret string) (jwt.MapClaims, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } return []byte(secret), nil }) if err != nil { return nil, err } if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { return claims, nil } return nil, jwt.ErrInvalidKey }