|
@@ -671,6 +671,19 @@ func (s *Server) OAuthLogin(c *gin.Context) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // TikTok 特殊处理:需要用 code 换取 access_token 和用户信息
|
|
|
|
|
+ if req.Provider == "tiktok" && req.Extra != "" {
|
|
|
|
|
+ tiktokUser, err := s.getTiktokUserInfo(req.OpenId, req.Extra)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ fmt.Printf("TikTok get user info error: %v\n", err)
|
|
|
|
|
+ ctx.Fail("tiktok_auth_failed")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ req.OpenId = tiktokUser.ID
|
|
|
|
|
+ req.Nickname = tiktokUser.Name
|
|
|
|
|
+ req.Avatar = tiktokUser.Picture
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Zalo 特殊处理:需要用 code 换取 access_token
|
|
// Zalo 特殊处理:需要用 code 换取 access_token
|
|
|
if req.Provider == "zalo" && req.Extra != "" {
|
|
if req.Provider == "zalo" && req.Extra != "" {
|
|
|
zaloUser, err := s.getZaloUserInfo(req.OpenId, req.Extra)
|
|
zaloUser, err := s.getZaloUserInfo(req.OpenId, req.Extra)
|
|
@@ -919,3 +932,109 @@ func (s *Server) getZaloUserInfo(code, codeVerifier string) (*ZaloUser, error) {
|
|
|
Picture: userInfo.Picture.Data.URL,
|
|
Picture: userInfo.Picture.Data.URL,
|
|
|
}, nil
|
|
}, nil
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// TiktokUser TikTok用户信息
|
|
|
|
|
+type TiktokUser struct {
|
|
|
|
|
+ ID string `json:"id"`
|
|
|
|
|
+ Name string `json:"name"`
|
|
|
|
|
+ Picture string `json:"picture"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// getTiktokUserInfo 使用code获取TikTok用户信息
|
|
|
|
|
+func (s *Server) getTiktokUserInfo(code, redirectUri string) (*TiktokUser, error) {
|
|
|
|
|
+ db := s.DB()
|
|
|
|
|
+
|
|
|
|
|
+ // 获取 TikTok Client Key 和 Secret
|
|
|
|
|
+ var clientKeyConfig, clientSecretConfig entity.DtConfig
|
|
|
|
|
+ if err := db.Where("`key` = ?", entity.ConfigKeyTiktokClientKey).First(&clientKeyConfig).Error; err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("tiktok client_key not configured")
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := db.Where("`key` = ?", entity.ConfigKeyTiktokClientSecret).First(&clientSecretConfig).Error; err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("tiktok client_secret not configured")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 用 code 换取 access_token
|
|
|
|
|
+ tokenUrl := "https://open.tiktokapis.com/v2/oauth/token/"
|
|
|
|
|
+ data := url.Values{}
|
|
|
|
|
+ data.Set("client_key", clientKeyConfig.Value)
|
|
|
|
|
+ data.Set("client_secret", clientSecretConfig.Value)
|
|
|
|
|
+ data.Set("code", code)
|
|
|
|
|
+ data.Set("grant_type", "authorization_code")
|
|
|
|
|
+ data.Set("redirect_uri", redirectUri)
|
|
|
|
|
+
|
|
|
|
|
+ req, _ := http.NewRequest("POST", tokenUrl, strings.NewReader(data.Encode()))
|
|
|
|
|
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
|
+
|
|
|
|
|
+ client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
|
+ resp, err := client.Do(req)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("request token failed: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer resp.Body.Close()
|
|
|
|
|
+
|
|
|
|
|
+ body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
+ fmt.Printf("TikTok token response: %s\n", string(body))
|
|
|
|
|
+
|
|
|
|
|
+ var tokenResp struct {
|
|
|
|
|
+ AccessToken string `json:"access_token"`
|
|
|
|
|
+ OpenId string `json:"open_id"`
|
|
|
|
|
+ ExpiresIn int `json:"expires_in"`
|
|
|
|
|
+ TokenType string `json:"token_type"`
|
|
|
|
|
+ Error string `json:"error"`
|
|
|
|
|
+ ErrorDesc string `json:"error_description"`
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := json.Unmarshal(body, &tokenResp); err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("parse token response failed: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if tokenResp.Error != "" || tokenResp.AccessToken == "" {
|
|
|
|
|
+ return nil, fmt.Errorf("get token failed: %s - %s", tokenResp.Error, tokenResp.ErrorDesc)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 用 access_token 获取用户信息
|
|
|
|
|
+ userUrl := "https://open.tiktokapis.com/v2/user/info/?fields=open_id,display_name,avatar_url"
|
|
|
|
|
+ userReq, _ := http.NewRequest("GET", userUrl, nil)
|
|
|
|
|
+ userReq.Header.Set("Authorization", "Bearer "+tokenResp.AccessToken)
|
|
|
|
|
+
|
|
|
|
|
+ userResp, err := client.Do(userReq)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("request user info failed: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer userResp.Body.Close()
|
|
|
|
|
+
|
|
|
|
|
+ userBody, _ := io.ReadAll(userResp.Body)
|
|
|
|
|
+ fmt.Printf("TikTok user response: %s\n", string(userBody))
|
|
|
|
|
+
|
|
|
|
|
+ var userInfoResp struct {
|
|
|
|
|
+ Data struct {
|
|
|
|
|
+ User struct {
|
|
|
|
|
+ OpenId string `json:"open_id"`
|
|
|
|
|
+ DisplayName string `json:"display_name"`
|
|
|
|
|
+ AvatarUrl string `json:"avatar_url"`
|
|
|
|
|
+ } `json:"user"`
|
|
|
|
|
+ } `json:"data"`
|
|
|
|
|
+ Error struct {
|
|
|
|
|
+ Code string `json:"code"`
|
|
|
|
|
+ Message string `json:"message"`
|
|
|
|
|
+ } `json:"error"`
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := json.Unmarshal(userBody, &userInfoResp); err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("parse user info failed: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if userInfoResp.Error.Code != "" && userInfoResp.Error.Code != "ok" {
|
|
|
|
|
+ return nil, fmt.Errorf("get user info failed: [%s] %s", userInfoResp.Error.Code, userInfoResp.Error.Message)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ openId := userInfoResp.Data.User.OpenId
|
|
|
|
|
+ if openId == "" {
|
|
|
|
|
+ openId = tokenResp.OpenId
|
|
|
|
|
+ }
|
|
|
|
|
+ if openId == "" {
|
|
|
|
|
+ return nil, fmt.Errorf("get user info failed: empty open_id")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return &TiktokUser{
|
|
|
|
|
+ ID: openId,
|
|
|
|
|
+ Name: userInfoResp.Data.User.DisplayName,
|
|
|
|
|
+ Picture: userInfoResp.Data.User.AvatarUrl,
|
|
|
|
|
+ }, nil
|
|
|
|
|
+}
|