Kaynağa Gözat

feat(oauth): migrate GitHub user identification from login to numeric ID

CaIon 1 ay önce
ebeveyn
işleme
632baadb57
3 değiştirilmiş dosya ile 50 ekleme ve 7 silme
  1. 31 2
      controller/oauth.go
  2. 8 0
      model/user.go
  3. 11 5
      oauth/github.go

+ 31 - 2
controller/oauth.go

@@ -1,6 +1,7 @@
 package controller
 
 import (
+	"fmt"
 	"net/http"
 	"strconv"
 
@@ -147,11 +148,18 @@ func handleOAuthBind(c *gin.Context, provider oauth.Provider) {
 		return
 	}
 
-	// Check if this OAuth account is already bound
+	// Check if this OAuth account is already bound (check both new ID and legacy ID)
 	if provider.IsUserIDTaken(oauthUser.ProviderUserID) {
 		common.ApiErrorI18n(c, i18n.MsgOAuthAlreadyBound, providerParams(provider.GetName()))
 		return
 	}
+	// Also check legacy ID to prevent duplicate bindings during migration period
+	if legacyID, ok := oauthUser.Extra["legacy_id"].(string); ok && legacyID != "" {
+		if provider.IsUserIDTaken(legacyID) {
+			common.ApiErrorI18n(c, i18n.MsgOAuthAlreadyBound, providerParams(provider.GetName()))
+			return
+		}
+	}
 
 	// Get current user from session
 	session := sessions.Default(c)
@@ -178,7 +186,7 @@ func handleOAuthBind(c *gin.Context, provider oauth.Provider) {
 func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *oauth.OAuthUser, session sessions.Session) (*model.User, error) {
 	user := &model.User{}
 
-	// Check if user already exists
+	// Check if user already exists with new ID
 	if provider.IsUserIDTaken(oauthUser.ProviderUserID) {
 		provider.SetProviderUserID(user, oauthUser.ProviderUserID)
 		err := provider.FillUserByProviderID(user, oauthUser.ProviderUserID)
@@ -192,6 +200,27 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
 		return user, nil
 	}
 
+	// Try to find user with legacy ID (for GitHub migration from login to numeric ID)
+	if legacyID, ok := oauthUser.Extra["legacy_id"].(string); ok && legacyID != "" {
+		if provider.IsUserIDTaken(legacyID) {
+			provider.SetProviderUserID(user, legacyID)
+			err := provider.FillUserByProviderID(user, legacyID)
+			if err != nil {
+				return nil, err
+			}
+			if user.Id != 0 {
+				// Found user with legacy ID, migrate to new ID
+				common.SysLog(fmt.Sprintf("[OAuth] Migrating user %d from legacy_id=%s to new_id=%s",
+					user.Id, legacyID, oauthUser.ProviderUserID))
+				if err := user.UpdateGitHubId(oauthUser.ProviderUserID); err != nil {
+					common.SysError(fmt.Sprintf("[OAuth] Failed to migrate user %d: %s", user.Id, err.Error()))
+					// Continue with login even if migration fails
+				}
+				return user, nil
+			}
+		}
+	}
+
 	// User doesn't exist, create new user if registration is enabled
 	if !common.RegisterEnabled {
 		return nil, &OAuthRegistrationDisabledError{}

+ 8 - 0
model/user.go

@@ -540,6 +540,14 @@ func (user *User) FillUserByGitHubId() error {
 	return nil
 }
 
+// UpdateGitHubId updates the user's GitHub ID (used for migration from login to numeric ID)
+func (user *User) UpdateGitHubId(newGitHubId string) error {
+	if user.Id == 0 {
+		return errors.New("user id is empty")
+	}
+	return DB.Model(user).Update("github_id", newGitHubId).Error
+}
+
 func (user *User) FillUserByDiscordId() error {
 	if user.DiscordId == "" {
 		return errors.New("discord id 为空!")

+ 11 - 5
oauth/github.go

@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"strconv"
 	"time"
 
 	"github.com/QuantumNous/new-api/common"
@@ -29,7 +30,8 @@ type gitHubOAuthResponse struct {
 }
 
 type gitHubUser struct {
-	Login string `json:"login"`
+	Id    int64  `json:"id"`    // GitHub numeric ID (permanent, never changes)
+	Login string `json:"login"` // GitHub username (can be changed by user)
 	Name  string `json:"name"`
 	Email string `json:"email"`
 }
@@ -127,18 +129,22 @@ func (p *GitHubProvider) GetUserInfo(ctx context.Context, token *OAuthToken) (*O
 		return nil, err
 	}
 
-	if githubUser.Login == "" {
-		logger.LogError(ctx, "[OAuth-GitHub] GetUserInfo failed: empty login field")
+	if githubUser.Id == 0 || githubUser.Login == "" {
+		logger.LogError(ctx, "[OAuth-GitHub] GetUserInfo failed: empty id or login field")
 		return nil, NewOAuthError(i18n.MsgOAuthUserInfoEmpty, map[string]any{"Provider": "GitHub"})
 	}
 
-	logger.LogDebug(ctx, "[OAuth-GitHub] GetUserInfo success: login=%s, name=%s, email=%s", githubUser.Login, githubUser.Name, githubUser.Email)
+	logger.LogDebug(ctx, "[OAuth-GitHub] GetUserInfo success: id=%d, login=%s, name=%s, email=%s",
+		githubUser.Id, githubUser.Login, githubUser.Name, githubUser.Email)
 
 	return &OAuthUser{
-		ProviderUserID: githubUser.Login,
+		ProviderUserID: strconv.FormatInt(githubUser.Id, 10), // Use numeric ID as primary identifier
 		Username:       githubUser.Login,
 		DisplayName:    githubUser.Name,
 		Email:          githubUser.Email,
+		Extra: map[string]any{
+			"legacy_id": githubUser.Login, // Store login for migration from old accounts
+		},
 	}, nil
 }