Files
gobeyhan/app/account/handlers/auth_handler.go
Beyhan Oğur f34e54c5a5 first commit
2026-04-26 21:43:40 +03:00

362 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}