package handlers import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "gobeyhan/app/account/services" settingsServices "gobeyhan/app/settings/services" "gobeyhan/config" "gobeyhan/database/models" "github.com/gin-gonic/gin" ) type AuthHandler struct { userService *services.UserService jwtService *settingsServices.JWTService } func NewAuthHandler(userService *services.UserService, jwtService *settingsServices.JWTService) *AuthHandler { return &AuthHandler{ userService: userService, jwtService: jwtService, } } // Register godoc // @Summary Register a new user // @Description Create a new user account with email and password // @Tags auth // @Accept json // @Produce json // @Param request body object{email=string,password=string,username=string} true "Registration data" // @Success 201 {object} object{token=string,user=models.User} // @Failure 400 {object} object{error=string} // @Router /api/v1/auth/register [post] func (h *AuthHandler) Register(c *gin.Context) { var input struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` Username string `json:"username" binding:"required,min=3"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Create user (password will be hashed by UserService) user := &models.User{ Email: input.Email, UserName: input.Username, } // Password is passed separately to be hashed if err := h.userService.CreateUser(user, input.Password); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Assign default role if err := h.userService.AssignDefaultRole(user.ID); err != nil { // Log error but don't fail registration // log.Printf("Failed to assign default role: %v", err) } // Generate JWT tokens accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"}) return } // Return tokens and user (without password) user.Password = "" c.JSON(http.StatusCreated, gin.H{ "access_token": accessToken, "refresh_token": refreshToken, "user": user, }) } // Login godoc // @Summary Login user // @Description Login with email and password // @Tags auth // @Accept json // @Produce json // @Param request body object{email=string,password=string} true "Login credentials" // @Success 200 {object} object{token=string,user=models.User} // @Failure 400 {object} object{error=string} // @Failure 401 {object} object{error=string} // @Router /api/v1/auth/login [post] func (h *AuthHandler) Login(c *gin.Context) { var input struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` TurnstileToken string `json:"turnstile_token"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Verify Turnstile if !verifyTurnstile(input.TurnstileToken) { c.JSON(http.StatusBadRequest, gin.H{"error": "Turnstile doğrulaması başarısız"}) return } // Get user by email user, err := h.userService.GetUserByEmail(input.Email) if err != nil || user == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) return } // Verify password if !h.userService.VerifyPassword(user.Password, input.Password) { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) return } // Generate JWT tokens accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"}) return } // Set refresh token as HttpOnly cookie (secure, XSS-safe) cookie := &http.Cookie{ Name: "refresh_token", Value: refreshToken, Path: "/", Domain: "localhost", // Explicitly set for local dev MaxAge: 7 * 24 * 60 * 60, // 7 days Secure: false, // Set true for HTTPS in production HttpOnly: true, // Cannot be accessed by JavaScript SameSite: http.SameSiteLaxMode, // Lax is better for local dev } http.SetCookie(c.Writer, cookie) fmt.Printf("[DEBUG] Login - Set-Cookie Raw: %s\n", cookie.String()) // Return tokens and user (without password) user.Password = "" c.JSON(http.StatusOK, gin.H{ "access_token": accessToken, "refresh_token": refreshToken, // Also in response for fallback "user": user, }) } // GetCurrentUser godoc // @Summary Get current user // @Description Get current authenticated user information // @Tags auth // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} models.User // @Failure 401 {object} object{error=string} // @Router /api/v1/auth/me [get] func (h *AuthHandler) GetCurrentUser(c *gin.Context) { // Get user ID from context (set by auth middleware) userIDStr, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) return } // Convert to uint64 var userID uint64 switch v := userIDStr.(type) { case string: parsed, _ := strconv.ParseUint(v, 10, 64) userID = parsed case uint64: userID = v case int: userID = uint64(v) case float64: userID = uint64(v) } // Get user from database user, err := h.userService.GetUserByID(userID) if err != nil || user == nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } // Return user (without password) user.Password = "" c.JSON(http.StatusOK, user) } // Logout godoc // @Summary Logout user // @Description Logout (client-side token removal) // @Tags auth // @Accept json // @Produce json // @Success 200 {object} object{message=string} // @Router /api/v1/auth/logout [post] func (h *AuthHandler) Logout(c *gin.Context) { // Clear refresh token cookie cookie := &http.Cookie{ Name: "refresh_token", Value: "", Path: "/", Domain: "localhost", MaxAge: -1, // Delete cookie HttpOnly: true, SameSite: http.SameSiteLaxMode, } http.SetCookie(c.Writer, cookie) fmt.Printf("[DEBUG] Logout - Set-Cookie Raw: %s\n", cookie.String()) // For JWT, logout is typically handled client-side // Server can implement token blacklisting if needed c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"}) } // RefreshToken godoc // @Summary Refresh access token // @Description Get a new access token using refresh token from HttpOnly cookie // @Tags auth // @Accept json // @Produce json // @Success 200 {object} object{access_token=string,refresh_token=string} // @Failure 401 {object} object{error=string} // @Router /api/v1/auth/refresh [post] func (h *AuthHandler) RefreshToken(c *gin.Context) { // Get refresh token from HttpOnly cookie refreshToken, err := c.Cookie("refresh_token") if err != nil { fmt.Printf("[DEBUG] RefreshToken - Cookie Error: %v\n", err) } else { fmt.Printf("[DEBUG] RefreshToken - Cookie Found: %s...\n", refreshToken[:10]) } if err != nil || refreshToken == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Refresh token not found"}) return } // Validate refresh token and get user ID claims, err := h.jwtService.ValidateToken(refreshToken) if err != nil { fmt.Printf("[DEBUG] RefreshToken - ValidateToken Error: %v\n", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired refresh token"}) return } // Get user ID from claims userID, err := claims.GetSubject() if err != nil { fmt.Printf("[DEBUG] RefreshToken - GetSubject Error: %v\n", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"}) return } // Parse user ID to uint64 // Sscan bazen boşluk vs. yüzünden hata verebilir, strconv daha güvenli parsedUint, err := strconv.ParseUint(userID, 10, 64) if err != nil { fmt.Printf("[DEBUG] RefreshToken - ParseUint Error: %v (userID=%s)\n", err, userID) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user ID format"}) return } uid := parsedUint // Get user from database user, err := h.userService.GetUserByID(uid) if err != nil { fmt.Printf("[DEBUG] RefreshToken - GetUserByID FAILED: %v\n", err) c.JSON(http.StatusUnauthorized, gin.H{"error": "Database error"}) return } if user == nil { fmt.Printf("[DEBUG] RefreshToken - User not found in DB for ID: %d\n", uid) c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"}) return } fmt.Printf("[DEBUG] RefreshToken - User found: %s\n", user.Email) // Generate new token pair newAccessToken, newRefreshToken, err := h.jwtService.GenerateTokenPair(*user) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate tokens"}) return } // Update refresh token cookie (token rotation) cookie := &http.Cookie{ Name: "refresh_token", Value: newRefreshToken, Path: "/", Domain: "localhost", MaxAge: 7 * 24 * 60 * 60, // 7 days Secure: false, HttpOnly: true, SameSite: http.SameSiteLaxMode, } http.SetCookie(c.Writer, cookie) // Return new access token and user info user.Password = "" // Ensure password is not sent c.JSON(http.StatusOK, gin.H{ "access_token": newAccessToken, "refresh_token": newRefreshToken, "user": user, // Critical for frontend session restore }) } // Helper: Verify Turnstile Token func verifyTurnstile(token string) bool { secret := config.AppConfig.TurnstileSecretKey if secret == "" { fmt.Println("[WARNING] Turnstile Secret Key not configured, skipping validation") return true // Skip validation if not configured } if token == "" { // If secret is configured, token is mandatory return false } formData := url.Values{ "secret": {secret}, "response": {token}, } resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", formData) if err != nil { fmt.Printf("[ERROR] Turnstile request failed: %v\n", err) return false } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { fmt.Printf("[ERROR] Failed to read Turnstile response: %v\n", err) return false } var result struct { Success bool `json:"success"` } if err := json.Unmarshal(body, &result); err != nil { fmt.Printf("[ERROR] Failed to parse Turnstile response: %v\n", err) return false } return result.Success }