From 018f80098e35ba869df50fa5274d730bad6e6cd7 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 30 Sep 2025 03:19:33 +0500 Subject: [PATCH] modified: internal/config/oauth.go modified: internal/handlers/oauth.go new file: internal/handlers/oauth_VK.go new file: internal/handlers/oauth_yandex.go modified: internal/utils/oauth_utils.go add oauth for VK and ynadex and google --- serv_golang_rest_api/internal/config/oauth.go | 4 +- .../internal/handlers/oauth.go | 55 ++++---- .../internal/handlers/oauth_VK.go | 125 ++++++++++++++++++ .../internal/handlers/oauth_yandex.go | 101 ++++++++++++++ .../internal/utils/oauth_utils.go | 72 +++++++++- 5 files changed, 325 insertions(+), 32 deletions(-) create mode 100644 serv_golang_rest_api/internal/handlers/oauth_VK.go create mode 100644 serv_golang_rest_api/internal/handlers/oauth_yandex.go diff --git a/serv_golang_rest_api/internal/config/oauth.go b/serv_golang_rest_api/internal/config/oauth.go index e11f1d4..be0d5cb 100644 --- a/serv_golang_rest_api/internal/config/oauth.go +++ b/serv_golang_rest_api/internal/config/oauth.go @@ -8,6 +8,7 @@ import ( "golang.org/x/oauth2/vk" ) + var ( GoogleOAuthConfig = &oauth2.Config{ ClientID: "your-google-client-id", @@ -21,6 +22,7 @@ var ( ClientID: "your-yandex-client-id", ClientSecret: "your-yandex-client-secret", RedirectURL: "http://localhost:8080/auth/yandex/callback", + Scopes: []string{"login:email", "login:info", "login:avatar"}, Endpoint: yandex.Endpoint, } @@ -28,7 +30,7 @@ var ( ClientID: "your-vk-client-id", ClientSecret: "your-vk-client-secret", RedirectURL: "http://localhost:8080/auth/vk/callback", + Scopes: []string{"email", "photos"}, Endpoint: vk.Endpoint, - Scopes: []string{"email"}, } ) \ No newline at end of file diff --git a/serv_golang_rest_api/internal/handlers/oauth.go b/serv_golang_rest_api/internal/handlers/oauth.go index 9957207..df89843 100644 --- a/serv_golang_rest_api/internal/handlers/oauth.go +++ b/serv_golang_rest_api/internal/handlers/oauth.go @@ -2,13 +2,14 @@ package handlers import ( - "encoding/json" - "net/http" - "serv_golang_rest_api/internal/config" - "serv_golang_rest_api/internal/models" - "serv_golang_rest_api/internal/utils" - - "gorm.io/gorm" + "encoding/json" + "net/http" + "serv_golang_rest_api/internal/config" + "serv_golang_rest_api/internal/models" + "serv_golang_rest_api/internal/utils" + + "golang.org/x/oauth2" + "gorm.io/gorm" ) type OAuthHandler struct { @@ -21,14 +22,6 @@ type GoogleUserInfo struct { Name string `json:"name"` } -type VKUserInfo struct { - Response []struct { - ID int `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - } `json:"response"` -} func (h *OAuthHandler) GoogleLogin(w http.ResponseWriter, r *http.Request) { url := config.GoogleOAuthConfig.AuthCodeURL("state") @@ -59,7 +52,7 @@ func (h *OAuthHandler) GoogleCallback(w http.ResponseWriter, r *http.Request) { } // Создаем или находим пользователя - user, err := h.findOrCreateOAuthUser("google", userInfo.ID, userInfo.Email, userInfo.Name) + user, err := h.findOrCreateOAuthUser("google", userInfo.ID, userInfo.Email, userInfo.Name, token) if err != nil { utils.WriteError(w, http.StatusInternalServerError, "Error processing user") return @@ -80,16 +73,22 @@ func (h *OAuthHandler) GoogleCallback(w http.ResponseWriter, r *http.Request) { // Аналогичные методы для Yandex и VK... -func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name string) (*models.User, error) { +func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name string, token *oauth2.Token) (*models.User, error) { var oauthProvider models.OAuthProvider - // Ищем существующую привязку OAuth err := h.DB.Where("provider = ? AND provider_id = ?", provider, providerID). Preload("User"). First(&oauthProvider).Error if err == nil { - // Нашли привязку, теперь загружаем пользователя + // Обновляем токены существующей привязки + oauthProvider.AccessToken = token.AccessToken + oauthProvider.RefreshToken = token.RefreshToken + oauthProvider.ExpiresAt = token.Expiry + if err := h.DB.Save(&oauthProvider).Error; err != nil { + return nil, err + } + var user models.User if err := h.DB.First(&user, oauthProvider.UserID).Error; err != nil { return nil, err @@ -97,16 +96,15 @@ func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name s return &user, nil } - // Если привязки нет, ищем пользователя по email + // Ищем пользователя по email var user models.User err = h.DB.Where("email = ?", email).First(&user).Error if err != nil { // Создаем нового пользователя user = models.User{ - Email: email, - Name: name, - // Генерируем случайный пароль для OAuth пользователей + Email: email, + Name: name, Password: utils.GenerateRandomPassword(), } if err := h.DB.Create(&user).Error; err != nil { @@ -114,11 +112,14 @@ func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name s } } - // Создаем привязку OAuth + // Создаем новую привязку OAuth с токенами oauthProvider = models.OAuthProvider{ - UserID: user.ID, - Provider: provider, - ProviderID: providerID, + UserID: user.ID, + Provider: provider, + ProviderID: providerID, + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + ExpiresAt: token.Expiry, } if err := h.DB.Create(&oauthProvider).Error; err != nil { diff --git a/serv_golang_rest_api/internal/handlers/oauth_VK.go b/serv_golang_rest_api/internal/handlers/oauth_VK.go new file mode 100644 index 0000000..41bc963 --- /dev/null +++ b/serv_golang_rest_api/internal/handlers/oauth_VK.go @@ -0,0 +1,125 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "serv_golang_rest_api/internal/config" + "serv_golang_rest_api/internal/utils" +) + +// VKUserInfo представляет данные пользователя от VK +type VKUserInfo struct { + Response []struct { + ID int `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Photo string `json:"photo_200"` + } `json:"response"` +} + +// VKEmailResponse представляет ответ с email от VK +type VKEmailResponse struct { + Email string `json:"email"` +} + +// VKLogin initiates VK OAuth flow +func (h *OAuthHandler) VKLogin(w http.ResponseWriter, r *http.Request) { + url := config.VKOAuthConfig.AuthCodeURL("state") + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +// VKCallback handles VK OAuth callback +func (h *OAuthHandler) VKCallback(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + + token, err := config.VKOAuthConfig.Exchange(r.Context(), code) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token: "+err.Error()) + return + } + + // VK не возвращает email в основном токене, нужно получить его отдельно + email, err := h.getVKEmail(token.AccessToken) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "Failed to get email from VK: "+err.Error()) + return + } + + client := config.VKOAuthConfig.Client(r.Context(), token) + + // Получаем основную информацию о пользователе + userInfoURL := fmt.Sprintf("https://api.vk.com/method/users.get?fields=photo_200,email&v=5.131&access_token=%s", token.AccessToken) + resp, err := client.Get(userInfoURL) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "Failed to get user info: "+err.Error()) + return + } + defer resp.Body.Close() + + var vkUserInfo VKUserInfo + if err := json.NewDecoder(resp.Body).Decode(&vkUserInfo); err != nil { + utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info: "+err.Error()) + return + } + + if len(vkUserInfo.Response) == 0 { + utils.WriteError(w, http.StatusBadRequest, "No user data received from VK") + return + } + + vkUser := vkUserInfo.Response[0] + userID := fmt.Sprintf("%d", vkUser.ID) + name := vkUser.FirstName + " " + vkUser.LastName + + // Используем email из отдельного запроса + if email == "" && vkUser.Email != "" { + email = vkUser.Email + } + + // Если email все еще пустой, создаем временный + if email == "" { + email = fmt.Sprintf("vk_%s@temp.vk", userID) + } + + // Создаем или находим пользователя + user, err := h.findOrCreateOAuthUser("vk", userID, email, name, token) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "Error processing user: "+err.Error()) + return + } + + jwtToken, err := utils.GenerateJWT(user.ID, user.Email) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "Error generating token: "+err.Error()) + return + } + + h.handleOAuthSuccess(w, r, jwtToken, user) +} + +// getVKEmail получает email из VK OAuth +func (h *OAuthHandler) getVKEmail(accessToken string) (string, error) { + // VK возвращает email в ответе на запрос токена, но если его нет, + // можно попробовать получить через API + emailURL := fmt.Sprintf("https://api.vk.com/method/account.getProfileInfo?v=5.131&access_token=%s", accessToken) + + resp, err := http.Get(emailURL) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var emailResp struct { + Response struct { + Email string `json:"email"` + } `json:"response"` + } + + if err := json.NewDecoder(resp.Body).Decode(&emailResp); err != nil { + return "", err + } + + return emailResp.Response.Email, nil +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/handlers/oauth_yandex.go b/serv_golang_rest_api/internal/handlers/oauth_yandex.go new file mode 100644 index 0000000..2b673d3 --- /dev/null +++ b/serv_golang_rest_api/internal/handlers/oauth_yandex.go @@ -0,0 +1,101 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "serv_golang_rest_api/internal/config" + "serv_golang_rest_api/internal/models" + "serv_golang_rest_api/internal/utils" + + "golang.org/x/oauth2" +) + +type YandexUserInfo struct { + ID string `json:"id"` + Login string `json:"login"` + Email string `json:"default_email"` + DisplayName string `json:"display_name"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + RealName string `json:"real_name"` + DefaultEmail string `json:"default_email"` + IsAvatarEmpty bool `json:"is_avatar_empty"` +} + +// YandexLogin initiates Yandex OAuth flow +func (h *OAuthHandler) YandexLogin(w http.ResponseWriter, r *http.Request) { + url := config.YandexOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline) + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +// YandexCallback handles Yandex OAuth callback +func (h *OAuthHandler) YandexCallback(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + if state != "state" { + utils.WriteError(w, http.StatusBadRequest, "Invalid state parameter") + return + } + + token, err := config.YandexOAuthConfig.Exchange(r.Context(), code) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token: "+err.Error()) + return + } + + client := config.YandexOAuthConfig.Client(r.Context(), token) + + // Получаем информацию о пользователе + resp, err := client.Get("https://login.yandex.ru/info?format=json") + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "Failed to get user info: "+err.Error()) + return + } + defer resp.Body.Close() + + var userInfo YandexUserInfo + if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { + utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info: "+err.Error()) + return + } + + // Формируем имя пользователя + name := h.getYandexUserName(userInfo) + + // Создаем или находим пользователя + user, err := h.findOrCreateOAuthUser("yandex", userInfo.ID, userInfo.DefaultEmail, name, token) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "Error processing user: "+err.Error()) + return + } + + jwtToken, err := utils.GenerateJWT(user.ID, user.Email) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "Error generating token: "+err.Error()) + return + } + + h.handleOAuthSuccess(w, r, jwtToken, user) +} + +func (h *OAuthHandler) handleOAuthSuccess(w http.ResponseWriter, r *http.Request, jwtToken string, user *models.User) { + panic("unimplemented") +} + +// getYandexUserName формирует имя пользователя из данных Yandex +func (h *OAuthHandler) getYandexUserName(userInfo YandexUserInfo) string { + if userInfo.RealName != "" { + return userInfo.RealName + } + if userInfo.DisplayName != "" { + return userInfo.DisplayName + } + if userInfo.FirstName != "" && userInfo.LastName != "" { + return userInfo.FirstName + " " + userInfo.LastName + } + if userInfo.FirstName != "" { + return userInfo.FirstName + } + return userInfo.Login +} diff --git a/serv_golang_rest_api/internal/utils/oauth_utils.go b/serv_golang_rest_api/internal/utils/oauth_utils.go index 205a0be..643f584 100644 --- a/serv_golang_rest_api/internal/utils/oauth_utils.go +++ b/serv_golang_rest_api/internal/utils/oauth_utils.go @@ -4,11 +4,75 @@ package utils import ( "crypto/rand" "fmt" + "serv_golang_rest_api/internal/models" + + "golang.org/x/oauth2" + "gorm.io/gorm" ) +type OAuthHandler struct { + DB *gorm.DB +} + // GenerateState generates a random state string for OAuth func GenerateState() string { - b := make([]byte, 16) - rand.Read(b) - return fmt.Sprintf("%x", b) -} \ No newline at end of file + b := make([]byte, 16) + rand.Read(b) + return fmt.Sprintf("%x", b) +} + +func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name string, token *oauth2.Token) (*models.User, error) { + var oauthProvider models.OAuthProvider + + err := h.DB.Where("provider = ? AND provider_id = ?", provider, providerID). + Preload("User"). + First(&oauthProvider).Error + + if err == nil { + // Обновляем токены существующей привязки + oauthProvider.AccessToken = token.AccessToken + oauthProvider.RefreshToken = token.RefreshToken + oauthProvider.ExpiresAt = token.Expiry + if err := h.DB.Save(&oauthProvider).Error; err != nil { + return nil, err + } + + var user models.User + if err := h.DB.First(&user, oauthProvider.UserID).Error; err != nil { + return nil, err + } + return &user, nil + } + + // Ищем пользователя по email + var user models.User + err = h.DB.Where("email = ?", email).First(&user).Error + + if err != nil { + // Создаем нового пользователя + user = models.User{ + Email: email, + Name: name, + Password: GenerateRandomPassword(), + } + if err := h.DB.Create(&user).Error; err != nil { + return nil, err + } + } + + // Создаем новую привязку OAuth с токенами + oauthProvider = models.OAuthProvider{ + UserID: user.ID, + Provider: provider, + ProviderID: providerID, + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + ExpiresAt: token.Expiry, + } + + if err := h.DB.Create(&oauthProvider).Error; err != nil { + return nil, err + } + + return &user, nil +}