package controllers import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strings" "time" database "goGin/app/database/config" "goGin/app/database/models" "goGin/app/middlewares" "goGin/app/services" configs "goGin/config" utils "goGin/pkg/utis" "github.com/gin-gonic/gin" "golang.org/x/oauth2" "golang.org/x/oauth2/github" "golang.org/x/oauth2/google" "gorm.io/gorm" ) // AuthResponse type AuthResponse struct { User UserResponse `json:"user"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } // RegisterPayload type RegisterPayload struct { UserName string `json:"username" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` } // LoginPayload type LoginPayload struct { Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` } // RefreshPayload type RefreshPayload struct { RefreshToken string `json:"refresh_token" binding:"required"` } // Helper to generate secure token for email verification func generateSecureToken() string { b := make([]byte, 32) rand.Read(b) return hex.EncodeToString(b) } // Register godoc // @Summary Register a new user // @Description Register a new user. Sends verification email. Does NOT return tokens. // @Tags auth // @Accept json // @Produce json // @Param register body RegisterPayload true "Register payload" // @Success 201 {object} controllers.AuthResponse // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/auth/register [post] func Register(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } var payload RegisterPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Check existing email var existing models.User if err := database.DB.Where("email = ?", payload.Email).First(&existing).Error; err == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "email already registered"}) return } hashedPwd, err := utils.HashPassword(payload.Password) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"}) return } // Email Verification Token verificationToken := generateSecureToken() emailVerified := false user := models.User{ UserName: payload.UserName, Email: payload.Email, Password: hashedPwd, EmailVerified: &emailVerified, EmailVerifyToken: verificationToken, } if err := database.DB.Create(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Send Verification Email go func() { if err := utils.SendVerificationEmail(user.Email, verificationToken); err != nil { fmt.Printf("Failed to send verification email to %s: %v\n", user.Email, err) } }() // Response c.JSON(http.StatusCreated, gin.H{ "message": "Registration successful. Please check your email to verify your account.", "user": toUserResponse(user), }) } // VerifyEmail godoc // @Summary Verify email address // @Description Verify email using token // @Tags auth // @Accept json // @Produce json // @Param token query string true "Verification Token" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /api/v1/auth/verify-email [get] func VerifyEmail(c *gin.Context) { token := c.Query("token") if token == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"}) return } var user models.User if err := database.DB.Where("email_verify_token = ?", token).First(&user).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or expired token"}) return } now := time.Now() verified := true user.EmailVerified = &verified user.EmailVerifiedAt = &now user.EmailVerifyToken = "" // Clear token if err := database.DB.Save(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify email"}) return } c.JSON(http.StatusOK, gin.H{"message": "Email verified successfully"}) } // Login godoc // @Summary Login user // @Description Login with email and password, returns tokens // @Tags auth // @Accept json // @Produce json // @Param login body LoginPayload true "Login payload" // @Success 200 {object} controllers.AuthResponse // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /api/v1/auth/login [post] func Login(c *gin.Context) { if database.DB == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "database not configured"}) return } var payload LoginPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var user models.User if err := database.DB.Where("email = ?", payload.Email).First(&user).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !utils.CheckPasswordHash(payload.Password, user.Password) { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } // Check if email is verified if user.EmailVerified != nil && !*user.EmailVerified { c.JSON(http.StatusUnauthorized, gin.H{"error": "email not verified"}) return } isAdmin := false if user.IsAdmin != nil && *user.IsAdmin { isAdmin = true } jwtService := services.NewJWTService() accessToken, err := jwtService.GenerateToken(user.ID, isAdmin) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate access token"}) return } refreshToken, err := jwtService.GenerateRefreshToken(user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"}) return } c.JSON(http.StatusOK, gin.H{ "user": toUserResponse(user), "access_token": accessToken, "refresh_token": refreshToken, }) } // Refresh godoc // @Summary Refresh access token // @Description usage: send refresh token to get new access token and refresh token // @Tags auth // @Accept json // @Produce json // @Param refresh body RefreshPayload true "Refresh token payload" // @Success 200 {object} map[string]string "Returns both access_token and refresh_token" // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /api/v1/auth/refresh [post] func Refresh(c *gin.Context) { var payload RefreshPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } jwtService := services.NewJWTService() claims, err := jwtService.ValidateToken(payload.RefreshToken) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"}) return } if claims.TokenType != "refresh" { c.JSON(http.StatusUnauthorized, gin.H{"error": "not a refresh token"}) return } // Get User var userID uint switch v := claims.UserID.(type) { case float64: userID = uint(v) case uint: userID = v default: c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user claim"}) return } var user models.User if err := database.DB.First(&user, userID).Error; err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"}) return } isAdmin := false if user.IsAdmin != nil && *user.IsAdmin { isAdmin = true } newAccessToken, err := jwtService.GenerateToken(user.ID, isAdmin) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"}) return } newRefreshToken, err := jwtService.GenerateRefreshToken(user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"}) return } c.JSON(http.StatusOK, gin.H{ "access_token": newAccessToken, "refresh_token": newRefreshToken, }) } // Me godoc // @Summary Get current user (me) // @Description Get current authenticated user information // @Tags auth // @Security BearerAuth // @Produce json // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/auth/me [get] func Me(c *gin.Context) { claims, ok := middlewares.GetAuthClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } var userID uint switch v := claims.UserID.(type) { case float64: userID = uint(v) case uint: userID = v default: c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid user claim"}) return } var user models.User if err := database.DB.First(&user, userID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } isAdmin := false if user.IsAdmin != nil && *user.IsAdmin { isAdmin = true } isVerified := false if user.EmailVerified != nil && *user.EmailVerified { isVerified = true } // Frontend'in beklediği formata göre response döndür c.JSON(http.StatusOK, gin.H{ "id": user.ID, "username": user.UserName, "email": user.Email, "email_verified": isVerified, "is_admin": isAdmin, }) } // OAuth Helpers var ( googleOauthConfig = &oauth2.Config{ RedirectURL: "", // Will be set in init or handler ClientID: "", ClientSecret: "", Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}, Endpoint: google.Endpoint, } githubOauthConfig = &oauth2.Config{ RedirectURL: "", ClientID: "", ClientSecret: "", Scopes: []string{"user:email"}, Endpoint: github.Endpoint, } ) func getGoogleConfig() *oauth2.Config { googleOauthConfig.ClientID = configs.AppConfig.GoogleClientID googleOauthConfig.ClientSecret = configs.AppConfig.GoogleClientSecret googleOauthConfig.RedirectURL = configs.AppConfig.GoogleRedirectURL return googleOauthConfig } func getGithubConfig() *oauth2.Config { githubOauthConfig.ClientID = configs.AppConfig.GithubClientID githubOauthConfig.ClientSecret = configs.AppConfig.GithubClientSecret githubOauthConfig.RedirectURL = configs.AppConfig.GithubRedirectURL return githubOauthConfig } // GoogleLogin godoc // @Summary Google OAuth2 Login // @Description Redirects to Google for authentication // @Tags auth // @Success 302 // @Router /api/v1/auth/google [get] func GoogleLogin(c *gin.Context) { url := getGoogleConfig().AuthCodeURL("state_google", oauth2.AccessTypeOffline) c.Redirect(http.StatusTemporaryRedirect, url) } // GoogleCallback godoc // @Summary Google OAuth2 Callback // @Description Handles Google OAuth2 callback // @Tags auth // @Success 200 {object} controllers.AuthResponse // @Failure 500 {object} map[string]string // @Router /api/v1/auth/google/callback [get] func GoogleCallback(c *gin.Context) { code := c.Query("code") token, err := getGoogleConfig().Exchange(context.Background(), code) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token: " + err.Error()}) return } client := getGoogleConfig().Client(context.Background(), token) resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info: " + err.Error()}) return } defer resp.Body.Close() userData, err := io.ReadAll(resp.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read user info"}) return } var googleUser struct { ID string `json:"id"` Email string `json:"email"` VerifiedEmail bool `json:"verified_email"` Name string `json:"name"` Picture string `json:"picture"` } if err := json.Unmarshal(userData, &googleUser); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse user info"}) return } handleSocialLogin(c, "google", googleUser.ID, googleUser.Email, googleUser.Name, googleUser.Picture) } // GithubLogin godoc // @Summary GitHub OAuth2 Login // @Description Redirects to GitHub for authentication // @Tags auth // @Success 302 // @Router /api/v1/auth/github [get] func GithubLogin(c *gin.Context) { url := getGithubConfig().AuthCodeURL("state_github", oauth2.AccessTypeOffline) c.Redirect(http.StatusTemporaryRedirect, url) } // GithubCallback godoc // @Summary GitHub OAuth2 Callback // @Description Handles GitHub OAuth2 callback // @Tags auth // @Success 200 {object} controllers.AuthResponse // @Failure 500 {object} map[string]string // @Router /api/v1/auth/github/callback [get] func GithubCallback(c *gin.Context) { code := c.Query("code") token, err := getGithubConfig().Exchange(context.Background(), code) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token: " + err.Error()}) return } client := getGithubConfig().Client(context.Background(), token) resp, err := client.Get("https://api.github.com/user") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user info: " + err.Error()}) return } defer resp.Body.Close() userData, err := io.ReadAll(resp.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read user info"}) return } var githubUser struct { ID int64 `json:"id"` Login string `json:"login"` Name string `json:"name"` Email string `json:"email"` AvatarURL string `json:"avatar_url"` } if err := json.Unmarshal(userData, &githubUser); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to parse user info"}) return } // GitHub email might be private, need to fetch separately if empty email := githubUser.Email if email == "" { // Fetch emails emailResp, err := client.Get("https://api.github.com/user/emails") if err == nil { defer emailResp.Body.Close() var emails []struct { Email string `json:"email"` Primary bool `json:"primary"` Verified bool `json:"verified"` } if body, err := io.ReadAll(emailResp.Body); err == nil { json.Unmarshal(body, &emails) for _, e := range emails { if e.Primary && e.Verified { email = e.Email break } } } } } if email == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Could not retrieve email from GitHub"}) return } handleSocialLogin(c, "github", fmt.Sprintf("%d", githubUser.ID), email, githubUser.Name, githubUser.AvatarURL) } func handleSocialLogin(c *gin.Context, provider, providerID, email, name, avatarURL string) { var user models.User var socialAccount models.SocialAccount // Check if social account exists err := database.DB.Where("provider = ? AND provider_id = ?", provider, providerID).First(&socialAccount).Error if err == nil { // Found social account, find user if err := database.DB.First(&user, socialAccount.UserID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "User record missing for social account"}) return } } else { // Social account not found. Check if email exists if err := database.DB.Where("email = ?", email).First(&user).Error; err == nil { // User exists, add social account newSocial := models.SocialAccount{ UserID: uint64(user.ID), Provider: provider, ProviderID: providerID, Email: email, Name: name, AvatarURL: avatarURL, } database.DB.Create(&newSocial) } else { // Create new user verified := true now := time.Now() // Generate random password randomPass := generateSecureToken() hashedPwd, _ := utils.HashPassword(randomPass) user = models.User{ UserName: name, // Handle duplicate usernames? Email: email, Password: hashedPwd, EmailVerified: &verified, EmailVerifiedAt: &now, } // Fallback username if empty if user.UserName == "" { user.UserName = strings.Split(email, "@")[0] } if err := database.DB.Create(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()}) return } newSocial := models.SocialAccount{ UserID: uint64(user.ID), Provider: provider, ProviderID: providerID, Email: email, Name: name, AvatarURL: avatarURL, } database.DB.Create(&newSocial) } } // Login logic isAdmin := false if user.IsAdmin != nil && *user.IsAdmin { isAdmin = true } jwtService := services.NewJWTService() accessToken, err := jwtService.GenerateToken(user.ID, isAdmin) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate access token"}) return } refreshToken, err := jwtService.GenerateRefreshToken(user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate refresh token"}) return } c.JSON(http.StatusOK, gin.H{ "user": toUserResponse(user), "access_token": accessToken, "refresh_token": refreshToken, }) }