package handlers import ( "context" "encoding/json" "fmt" "io" "net/http" "strconv" "gobeyhan/app/account/services" settingsServices "gobeyhan/app/settings/services" "gobeyhan/config" "gobeyhan/database/models" "github.com/gin-gonic/gin" "golang.org/x/oauth2" "golang.org/x/oauth2/github" "golang.org/x/oauth2/google" ) type OAuthHandler struct { userService *services.UserService socialAccountService *services.SocialAccountService jwtService *settingsServices.JWTService googleOAuthConfig *oauth2.Config githubOAuthConfig *oauth2.Config } func NewOAuthHandler( userService *services.UserService, socialAccountService *services.SocialAccountService, jwtService *settingsServices.JWTService, ) *OAuthHandler { // Google OAuth config googleConfig := &oauth2.Config{ ClientID: config.AppConfig.GoogleClientID, ClientSecret: config.AppConfig.GoogleClientSecret, RedirectURL: config.AppConfig.GoogleRedirectURL, Scopes: []string{ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", }, Endpoint: google.Endpoint, } // GitHub OAuth config githubConfig := &oauth2.Config{ ClientID: config.AppConfig.GithubClientID, ClientSecret: config.AppConfig.GithubClientSecret, RedirectURL: config.AppConfig.GithubRedirectURL, Scopes: []string{"user:email"}, Endpoint: github.Endpoint, } return &OAuthHandler{ userService: userService, socialAccountService: socialAccountService, jwtService: jwtService, googleOAuthConfig: googleConfig, githubOAuthConfig: githubConfig, } } // GoogleLogin godoc // @Summary Google OAuth login // @Description Redirect to Google OAuth // @Tags auth,oauth // @Produce json // @Router /api/v1/auth/google [get] func (h *OAuthHandler) GoogleLogin(c *gin.Context) { url := h.googleOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline) c.Redirect(http.StatusTemporaryRedirect, url) } // GoogleCallback godoc // @Summary Google OAuth callback // @Description Handle Google OAuth callback // @Tags auth,oauth // @Produce json // @Param code query string true "Authorization code" // @Success 200 {object} object{token=string,user=models.User} // @Failure 400 {object} object{error=string} // @Router /api/v1/auth/google/callback [get] func (h *OAuthHandler) GoogleCallback(c *gin.Context) { code := c.Query("code") if code == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Code not found"}) return } // Exchange code for token token, err := h.googleOAuthConfig.Exchange(context.Background(), code) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"}) return } // Get user info from Google client := h.googleOAuthConfig.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"}) return } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) var googleUser struct { ID string `json:"id"` Email string `json:"email"` VerifiedEmail bool `json:"verified_email"` Name string `json:"name"` Picture string `json:"picture"` } json.Unmarshal(data, &googleUser) // Find or create user user, accessToken, refreshToken, err := h.findOrCreateOAuthUser( googleUser.Email, googleUser.Name, "google", googleUser.ID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "token": accessToken, "refresh_token": refreshToken, "user": user, }) } // GithubLogin godoc // @Summary GitHub OAuth login // @Description Redirect to GitHub OAuth // @Tags auth,oauth // @Produce json // @Router /api/v1/auth/github [get] func (h *OAuthHandler) GithubLogin(c *gin.Context) { url := h.githubOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline) c.Redirect(http.StatusTemporaryRedirect, url) } // GithubCallback godoc // @Summary GitHub OAuth callback // @Description Handle GitHub OAuth callback // @Tags auth,oauth // @Produce json // @Param code query string true "Authorization code" // @Success 200 {object} object{token=string,user=models.User} // @Failure 400 {object} object{error=string} // @Router /api/v1/auth/github/callback [get] func (h *OAuthHandler) GithubCallback(c *gin.Context) { code := c.Query("code") if code == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Code not found"}) return } // Exchange code for token token, err := h.githubOAuthConfig.Exchange(context.Background(), code) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to exchange token"}) return } // Get user info from GitHub client := h.githubOAuthConfig.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"}) return } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) var githubUser struct { ID int `json:"id"` Login string `json:"login"` Email string `json:"email"` Name string `json:"name"` } json.Unmarshal(data, &githubUser) // If email is not public, fetch it separately if githubUser.Email == "" { emailResp, _ := client.Get("https://api.github.com/user/emails") if emailResp != nil { defer emailResp.Body.Close() emailData, _ := io.ReadAll(emailResp.Body) var emails []struct { Email string `json:"email"` Primary bool `json:"primary"` Verified bool `json:"verified"` } json.Unmarshal(emailData, &emails) for _, e := range emails { if e.Primary && e.Verified { githubUser.Email = e.Email break } } } } username := githubUser.Name if username == "" { username = githubUser.Login } // Find or create user user, accessToken, refreshToken, err := h.findOrCreateOAuthUser( githubUser.Email, username, "github", strconv.Itoa(githubUser.ID), ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "token": accessToken, "refresh_token": refreshToken, "user": user, }) } // findOrCreateOAuthUser finds existing user or creates new one for OAuth func (h *OAuthHandler) findOrCreateOAuthUser( email, username, provider, providerUserID string, ) (*models.User, string, string, error) { // Try to find existing user by email user, err := h.userService.GetUserByEmail(email) if err != nil { return nil, "", "", err } // If user doesn't exist, create new one if user == nil { user = &models.User{ Email: email, UserName: username, } // Create user with empty password if err := h.userService.CreateUser(user, ""); err != nil { return nil, "", "", fmt.Errorf("failed to create user: %w", err) } // Assign default role if err := h.userService.AssignDefaultRole(user.ID); err != nil { // Log error but continue // fmt.Printf("Failed to assign default role: %v\n", err) } } // Check if social account exists accounts, err := h.socialAccountService.GetSocialAccountsByUser(user.ID) if err != nil { return nil, "", "", err } // Create social account if it doesn't exist found := false for _, acc := range accounts { if acc.Provider == provider && acc.ProviderID == providerUserID { found = true break } } if !found { socialAccount := &models.SocialAccount{ UserID: user.ID, Provider: provider, ProviderID: providerUserID, } if err := h.socialAccountService.CreateSocialAccount(socialAccount); err != nil { return nil, "", "", fmt.Errorf("failed to create social account: %w", err) } } // Generate JWT tokens accessToken, refreshToken, err := h.jwtService.GenerateTokenPair(*user) if err != nil { return nil, "", "", fmt.Errorf("failed to generate tokens: %w", err) } // Clear password before returning user.Password = "" return user, accessToken, refreshToken, nil }