diff --git a/controller/oauth.go b/controller/oauth.go index a24912933..fb9e59a59 100644 --- a/controller/oauth.go +++ b/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{} diff --git a/model/user.go b/model/user.go index 395daa0b5..47508a0bb 100644 --- a/model/user.go +++ b/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 为空!") diff --git a/oauth/github.go b/oauth/github.go index d080ce54e..e38f8a784 100644 --- a/oauth/github.go +++ b/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 }