first commit
This commit is contained in:
297
app/account/handlers/oauth_handler.go
Normal file
297
app/account/handlers/oauth_handler.go
Normal file
@@ -0,0 +1,297 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user