Skip to content

fix: 使用GitHub数字ID代替可变的login作为唯一标识 - #2609

Closed
kosmoli wants to merge 4950 commits into
QuantumNous:mainfrom
kosmoli:fix/github-oauth-use-numeric-id
Closed

fix: 使用GitHub数字ID代替可变的login作为唯一标识#2609
kosmoli wants to merge 4950 commits into
QuantumNous:mainfrom
kosmoli:fix/github-oauth-use-numeric-id

Conversation

@kosmoli

@kosmoli kosmoli commented Jan 8, 2026

Copy link
Copy Markdown

🔒 安全修复:GitHub OAuth使用可变login字段导致用户数据丢失

🚨 严重安全漏洞

main分支存在多个严重的安全和架构问题:

  1. 核心架构缺陷:使用可变的GitHub login 字段(用户名)作为唯一标识
  2. 账户劫持风险:用户修改GitHub用户名后无法登录原账号
  3. 数据丢失:系统创建重复账号,用户失去所有数据
  4. 并发竞态条件GetMaxUserId()+1 导致用户名冲突
  5. URL路径遍历:未对username进行编码,存在安全风险
  6. SQL兼容性问题:使用MySQL特有语法,不支持其他数据库

📝 与main分支的详细对比

核心修改:controller/github.go

main分支的问题代码:

// ❌ 问题1:结构体缺少数字ID字段
type GitHubUser struct {
    Login string `json:"login"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func GitHubOAuth(c *gin.Context) {
    // ❌ 问题2:使用可变的login作为唯一标识
    user := model.User{
        GitHubId: githubUser.Login,  // 存储用户名
    }

    if model.IsGitHubIdAlreadyTaken(user.GitHubId) {
        // 登录现有账号
    } else {
        // ❌ 问题3:并发不安全的用户名生成
        user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
        user.Insert(inviterId)  // 可能因为并发失败
    }
}

// ❌ 问题4:验证逻辑不正确
if githubUser.Login == "" {  // 应该检查Id
    return nil, errors.New("返回值非法")
}

本PR的修复// ✅ 修复1:添加数字ID字段
type GitHubUser struct {
    Id    int64  `json:"id"`     // GitHub数字ID(永久不变)
    Login string `json:"login"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func GitHubOAuth(c *gin.Context) {
    // ✅ 修复2:使用不可变的数字ID
    numericId := strconv.FormatInt(githubUser.Id, 10)
    user := model.User{GitHubId: numericId}

    if model.IsGitHubIdAlreadyTaken(numericId) {
        // 登录现有账号
    } else {
        // ✅ 修复3:使用重试机制处理并发
        maxRetries := 5
        for retry := 0; retry < maxRetries; retry++ {
            user.Username = "github_" + strconv.Itoa(model.GetMaxUserId()+1)
            // ... 填充其他字段 ...

            insertErr := user.Insert(inviterId)
            if insertErr == nil {
                break  // 成功
            }
            // 如果不是重复键错误,直接返回
            if !isDuplicateKeyError(insertErr) {
                break
            }
            // 用户名重复,自动重试
        }
    }
}

// ✅ 修复4:验证数字ID
if githubUser.Id == 0 {
    return nil, errors.New("返回值非法")
}

新增精确的错误检测函数

main分支没有此函数或使用不精确的通用检查

本PR新增// isDuplicateKeyError 精确检测不同数据库的唯一约束冲突
// 通过检测数据库特定的错误码或消息来识别
func isDuplicateKeyError(err error) bool {
    errMsg := strings.ToLower(err.Error())

    // MySQL: Error 1062: Duplicate entry
    if strings.Contains(errMsg, "duplicate entry") ||
       strings.Contains(errMsg, "er_dup_entry") {
        return true
    }

    // PostgreSQL: SQLSTATE 23505 - unique_violation
    if strings.Contains(errMsg, "duplicate key value violates unique constraint") ||
       strings.Contains(errMsg, "sqlstate 23505") ||
       strings.Contains(errMsg, "23505") {
        return true
    }

    // SQLite: "UNIQUE constraint failed"
    if strings.Contains(errMsg, "unique constraint failed") {
        return true
    }

    return false  // 精确检测,避免误分类
}

改进点-移除了不精确的 "constraint" 检查
-针对MySQLPostgreSQLSQLite分别检测
-避免误分类其他类型的数据库错误

---
🔐 新增安全功能

1. 精确的数字ID验证

main分支无此功能

本PR新增// 在迁移工具中验证整个字符串是否为数字
// 使用ParseInt而不是只检查第一个字符
_, err := strconv.ParseInt(user.GitHubId, 10, 64)
if err == nil {
    // 确认是纯数字ID,跳过迁移
}

防止的问题:"123abc" 会被正确识别为未迁移而不是被误认为已迁移

2. URL路径遍历防护

main分支无此功能

本PR新增// 对username进行URL编码以防止路径遍历攻击
encodedUsername := url.PathEscape(username)
apiURL := fmt.Sprintf("https://api.github.com/users/%s", encodedUsername)

防止的攻击恶意构造的username如 "../../admin" 会被编码为 "%2E%2E%2E%2F%2E%2E%2Fadmin"

---
📦 新增文件和工具

1. docs/migrate_github_ids.sql

完整的数据库迁移脚本支持三种数据库特性-为MySQLPostgreSQLSQLite分别提供查询语法
-避免MySQL自引用子查询问题
-包含备份迁移验证回滚步骤
-详细的说明和示例

关键查询示例-- MySQL验证迁移结果
CASE WHEN github_id REGEXP '^[0-9]+$' THEN '✓ 已迁移'

-- PostgreSQL验证迁移结果
CASE WHEN github_id ~ '^[0-9]+$' THEN '✓ 已迁移'

-- SQLite验证迁移结果
CASE WHEN github_id GLOB '*[0-9]*' AND github_id NOT GLOB '*[a-zA-Z]*' THEN '✓ 已迁移'

2. docs/migrate_github_ids.go

功能完整的自动迁移工具核心功能-自动调用GitHub API获取数字ID
-支持GitHub Personal Access Token提高速率限制到5000次/小时-自动处理速率限制检查 X-RateLimit-* 响应头-智能重试机制最多3次指数退避-并发处理限制并发数为5-Unicode安全的字符处理
-精确的数字ID验证
-URL路径遍历防护

安全改进// 1. 精确的数字ID验证
_, err := strconv.ParseInt(user.GitHubId, 10, 64)
if err == nil {
    return  // 已迁移,跳过
}

// 2. URL编码防止路径遍历
encodedUsername := url.PathEscape(username)
apiURL := fmt.Sprintf("https://api.github.com/users/%s", encodedUsername)

// 3. Unicode安全的字符检查
if strings.HasPrefix(result, "✓ ") {
    successCount++
}

使用方法:
# 使用GitHub Token推荐export GITHUB_TOKEN=your_token
go run docs/migrate_github_ids.go

# 或使用命令行参数
go run docs/migrate_github_ids.go --token=your_token

# 预览模式
go run docs/migrate_github_ids.go --dry-run

---
⚠️ Breaking Change重大变更本PR不向后兼容main分支原因- main分支github_id = 用户名 "alice"- 本PRgithub_id = 数字ID "12345678"- 两者格式完全不同必须迁移数据

不迁移的严重后果-所有使用GitHub登录的旧用户无法登录
-用户创建新账号并失去所有数据
-生产环境中的API突然失效
-应用崩溃和数据丢失

---
🚀 完整部署流程必须按顺序步骤1备份数据库 ⚠️ 强制要求

# MySQL
mysqldump -u root -p new_api > backup_$(date +%Y%m%d_%H%M%S).sql

# PostgreSQL  
pg_dump new_api > backup_$(date +%Y%m%d_%H%M%S).sql

# SQLite
cp new-api.db backup_$(date +%Y%m%d_%H%M%S).db

步骤2运行迁移工具 ⚠️ 强制要求

推荐方式使用GitHub Token
# 1. 获取GitHub Token
# GitHub Settings -> Developer settings -> Personal access tokens -> Tokens (classic)
# 只需要 public_repo 权限

# 2. 设置token
export GITHUB_TOKEN=your_github_token

# 3. 预览模式查看会有哪些改动go run docs/migrate_github_ids.go --dry-run

# 4. 实际迁移
go run docs/migrate_github_ids.go

输出示例:
✓ 从环境变量 GITHUB_TOKEN 读取token使用GitHub token认证速率限制5000/小时找到 150 个使用GitHub登录的用户

  [GitHub API] 剩余请求: 4998, 重置时间: 15:04:05成功: alice (ID: 123) - alice12345678
  [GitHub API] 剩余请求: 4997, 重置时间: 15:04:05成功: bob (ID: 124) - bob87654321失败: charlie (ID: 125) - GitHub用户不存在: charlie (404)

========================================
迁移结果
========================================
总计: 150 | 成功: 148 | 跳过: 0 | 失败: 2
========================================

步骤3验证迁移结果

选择适合你数据库的查询MySQL:
SELECT
    COUNT(CASE WHEN github_id REGEXP '^[0-9]+$' THEN 1 END) as migrated,
    COUNT(CASE WHEN github_id NOT REGEXP '^[0-9]+$' THEN 1 END) as pending
FROM users WHERE github_id IS NOT NULL AND github_id != '';

PostgreSQL:
SELECT
    COUNT(CASE WHEN github_id ~ '^[0-9]+$' THEN 1 END) as migrated,
    COUNT(CASE WHEN github_id !~ '^[0-9]+$' THEN 1 END) as pending
FROM users WHERE github_id IS NOT NULL AND github_id != '';

SQLite:
SELECT
    COUNT(CASE WHEN github_id GLOB '*[0-9]*' AND github_id NOT GLOB '*[a-zA-Z]*' THEN 1 END) as migrated,
    COUNT(CASE WHEN NOT (github_id GLOB '*[0-9]*' AND github_id NOT GLOB '*[a-zA-Z]*') THEN 1 END) as pending
FROM users WHERE github_id IS NOT NULL AND github_id != '';

期望结果migrated = 总数, pending = 0

步骤4部署新代码

# 拉取本PR的代码
git pull origin main

# 重新编译
go build -o new-api

# 重启服务
systemctl restart new-api
# 
docker-compose down && docker-compose up -d

步骤5测试验证

1. 使用GitHub登录测试账号
2. 检查系统日志确认无错误
3. 监控用户登录成功率
4. 确认Token可以正常使用

---
📊 修改统计

| 文件                        | 修改类型 | 行数变化  | 说明          |
|-----------------------------|----------|-----------|---------------|
| controller/github.go        | 修改     | +96, -25  | 核心OAuth逻辑 |
| docs/migrate_github_ids.go  | 新增     | +385      | 迁移工具      |
| docs/migrate_github_ids.sql | 新增     | +282      | 迁移脚本      |
| 总计                        | -        | +763, -25 | -             |

---完整的测试清单

- 新用户注册使用数字ID- 用户登录只使用数字ID- 并发注册重试机制- GitHub账号绑定
- 迁移工具预览模式
- 迁移工具实际迁移
- GitHub API认证
- 速率限制处理
- 错误重试机制
- 数字ID精确验证
- URL编码防护
- Unicode字符处理
- 数据库错误检测
- SQL跨数据库兼容
- 代码格式检查
- 语法检查

---
🔐 安全性和稳定性对比

| 方面               | main分支        | 本PR           |
|--------------------|-----------------|----------------|
| 唯一标识符         |可变         |不可变      |
| 用户改GitHub用户名 |无法登录     |正常登录    |
| 并发注册           |冲突         |自动重试    |
| 错误检测           |/不精确    |精确检测    |
| URL安全            |路径遍历风险 |URL编码防护 |
| SQL兼容性          |仅MySQL      |三种数据库  |
| 数据丢失风险       |高风险       |无风险      |
| 迁移支持           |           |完整工具    |

---
📚 技术背景

GitHub API用户ID说明

- login字段用户名可以随时更改不唯一
- id字段数字ID永久不变全局唯一
- 官方文档https://docs.github.com/en/rest/users/users#about-user-ids

OAuth最佳实践

- 应使用不可变的用户标识符
- 不要使用可显示的用户名作为唯一标识
- 参考OWASP Insecure Direct Object Reference

与其他OAuth实现保持一致

| 提供商   | 唯一标识      | 稳定性           |
|----------|---------------|------------------|
| Discord  | UID (数字ID)  |不可变        |
| OIDC     | sub (Subject) |不可变        |
| Telegram | 数字ID        |不可变        |
| GitHub   | 数字ID        |不可变 (本PR) |

---
🙋 常见问题

Q: 为什么必须迁移数据A: main分支使用可变用户名作为唯一标识这是根本性架构缺陷必须迁移到数字ID才能彻底解决问题Q: 迁移需要多长时间A: 使用token认证5000/小时),通常几分钟建议先运行 --dry-run 预览Q: 如何获取GitHub tokenA:
1. GitHub SettingsDeveloper settingsPersonal access tokens
2. Tokens (classic) → Generate new token
3. 只需要 public_repo 权限
4. 设置环境变量export GITHUB_TOKEN=your_token

Q: 迁移失败怎么办A:
1. 查看工具输出的详细错误
2. 从备份恢复数据库
3. 排查问题后重新运行
4. 常见原因用户名已更改网络问题API限制

Q: 会影响其他OAuth提供商吗A: 不会本PR只影响GitHub OAuthDiscordOIDCTelegramWeChat等不受影响Q: 迁移过程中用户能登录吗A: 建议低峰期执行迁移时用户可能无法登录通常几分钟)。

Q: 如果已经出现重复账号A: 参考 docs/migrate_github_ids.sql 中的账号合并SQL脚本Q: 支持哪些数据库A: MySQLPostgreSQLSQLiteSQL脚本为每种数据库提供了专门的查询语法Q: 速率限制是多少A:
- 未认证60/小时
- 已认证5000/小时
- 工具会自动检测并显示剩余配额
---
⚠️ 重要提醒部署前必须备份数据库并运行迁移工具请审阅并合并此PR以彻底修复GitHub OAuth的用户数据丢失问题! 🚀

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
* GitHub authentication now uses numeric user IDs instead of login names for more reliable identity management.

* **Documentation**
* Added migration guide and utility tool for transitioning existing GitHub-based accounts to numeric IDs, including dry-run capability and concurrent processing support.

<sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

seefs001 and others added 30 commits November 24, 2025 14:06
fix: root page does not have analytic code
…ini-integration-011nJGemhrPUdqwg3qDvmqVB

feat: enable thoughtSignature for non-function-call messages
…pro-image-preview-oai

OAI生图接口支持gemini 3 pro image preview
…ageConfig

fix: gemini image correct generationConfig
…ith i18n

- Add SSEViewer component for interactive SSE message inspection
  * Display SSE data stream with collapsible panels
  * Show parsed JSON with syntax highlighting
  * Display key information badges (content, tokens, finish reason)
  * Support copy individual or all SSE messages
  * Show error messages with detailed information

- Support Ctrl+V to paste images in chat input
  * Enable image paste in CustomInputRender component
  * Auto-detect and add pasted images to image list
  * Show toast notifications for paste results

- Add complete i18n support for 6 languages
  * Chinese (zh): Complete translations
  * English (en): Complete translations
  * Japanese (ja): Add 28 new translations
  * French (fr): Add 28 new translations
  * Russian (ru): Add 28 new translations
  * Vietnamese (vi): Add 32 new translations

- Update .gitignore to exclude data directory
…-i2v

Gemini Veo3.1[AI Studio]增加图生视频支持
Ensure image file is closed using defer after opening.
…edit

Gemini Image系列支持图像编辑
…d-oai

feat: 视频下载和界面预览统一使用OAI标准接口
Calcium-Ion and others added 10 commits January 5, 2026 18:32
fix: fix the proxyURL is empty, not using the default HTTP client configuration && the AWS calling side did not apply the relay timeout.
fix: add tips for model management and channel testing
问题描述:
- 使用 auto 分组的令牌调用 /v1/videos 等 Task 接口时,虽然任务能成功创建,
  但使用日志不显示记录,且不会扣费

根本原因:
- Distribute 中间件在选择渠道后,会将实际选中的分组存储在 ContextKeyAutoGroup 中
- 但 RelayTaskSubmit 函数没有从 context 中读取这个值来更新 info.UsingGroup
- 导致 info.UsingGroup 始终是 "auto" 而不是实际选中的分组(如 "sora2逆")
- 当 auto 分组的倍率配置为 0 时,quota 计算结果为 0
- 日志记录条件 "if quota != 0" 不满足,导致日志不记录、不扣费

修复方案:
- 在 RelayTaskSubmit 函数中计算分组倍率之前,添加从 ContextKeyAutoGroup
  获取实际分组的逻辑
- 使用安全的类型断言,避免潜在的 panic 风险

影响范围:
- 仅影响 Task Relay 流程(/v1/videos, /suno, /kling 等接口)
- 不影响使用具体分组令牌的调用
- 不影响其他 Relay 类型(chat/completions 等已有类似处理逻辑)
…task-logging

fix(task): 修复使用 auto 分组时 Task Relay 不记录日志和不扣费的问题
@coderabbitai

coderabbitai Bot commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Switched GitHub identity handling from login strings to numeric GitHub IDs: added GitHubUser.Id int64, validate by numeric Id, store github_id as the stringified numeric id, updated OAuth and bind flows to use numeric IDs, added retry-on-duplicate-user creation with duplicate-key detection, and added migration tooling and docs.

Changes

Cohort / File(s) Summary
GitHub controller
controller/github.go
Added GitHubUser.Id int64. OAuth and bind flows now use stringified numeric GitHub IDs for lookups/persistence, new-user creation retries on username collisions (max 5), introduced isDuplicateKeyError helper, and adjusted control flow to prefer numeric-id based identity.
Migration tool (Go CLI)
docs/migrate_github_ids.go
New CLI to resolve numeric GitHub IDs via GitHub API and migrate existing github_id values; supports dry-run/live, token handling, concurrency, retry/backoff, confirmation prompts, and per-user reporting.
Migration docs / SQL
docs/migrate_github_ids.sql
New migration guide and SQL snippets covering backups, identifying affected users, migration approaches (CLI/manual/batch SQL), verification queries for MySQL/Postgres/SQLite, rollback steps, stats, and notes on idempotence.

Sequence Diagram(s)

sequenceDiagram
  actor Client
  participant Server
  participant GitHubAPI
  participant DB

  Client->>Server: OAuth callback (code)
  Server->>GitHubAPI: Exchange code -> access token
  GitHubAPI->>Server: Return access token
  Server->>GitHubAPI: Request user info (includes numeric id)
  GitHubAPI->>Server: Return user info (id, login, email)
  Server->>DB: Lookup user by github_id (stringified numeric id)
  alt user exists
    DB-->>Server: return user
    Server->>DB: update user fields as needed (ensure github_id numeric)
  else new user
    Server->>DB: create user (github_id = numeric id)
    DB-->>Server: success or unique-constraint error
    alt duplicate username error
      Server->>Server: retry username generation (up to 5) and re-attempt insert
    end
  end
  Server->>Client: Issue session / response
Loading
sequenceDiagram
  actor Client
  participant Server
  participant GitHubAPI
  participant DB

  Client->>Server: Bind request (OAuth code)
  Server->>GitHubAPI: Exchange code -> user info (numeric id)
  GitHubAPI->>Server: Return user info
  Server->>DB: Check if numeric github_id already bound
  alt bound to another account
    DB-->>Server: bound -> Server returns error
  else not bound
    Server->>DB: update current user set github_id = numeric id
    DB-->>Server: updated
    Server->>Client: success
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped from names to numbers bright,
IDs steady now, no fleeting sprite,
I nibbled at duplicates with care,
Retry hops fixed the username snare,
Migration carrots line the night.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: switching from mutable login to immutable numeric GitHub ID as the unique identifier.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9301263 and 9ce1074.

📒 Files selected for processing (1)
  • controller/github.go
🔇 Additional comments (2)
controller/github.go (2)

26-26: LGTM - Proper field addition for GitHub numeric ID.

The Id int64 field correctly captures GitHub's immutable numeric user identifier, which is the appropriate choice for unique user identification per OAuth best practices.


77-79: Validation change is safe and reliable.

The check githubUser.Id == 0 correctly validates against invalid responses. GitHub API always returns the id field in user objects, and since GitHub IDs are always positive integers, treating 0 as invalid is a proper safeguard against malformed or incomplete API responses. The change is appropriate.

Comment thread controller/github.go Outdated
@seefs001

seefs001 commented Jan 8, 2026

Copy link
Copy Markdown
Collaborator

好家伙,你是完全不管兼容的吗

@kosmoli
kosmoli force-pushed the fix/github-oauth-use-numeric-id branch from 9ce1074 to 2c54162 Compare January 8, 2026 13:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
controller/github.go (1)

113-163: Architectural recommendation: Use database migration script instead of runtime migration.

Given the security vulnerabilities and complexity of runtime migration, consider this approach:

  1. Before deployment:

    • Create a database migration script that updates all existing GitHubId values
    • The script should query GitHub API for each user's current numeric ID based on their stored login
    • Update the database records with the numeric IDs
    • Notify all users of the maintenance window
  2. After migration:

    • Deploy the simplified code that only looks up by numeric ID
    • Remove all compatibility fallback logic
    • Clean, secure, and maintainable
  3. For users who can't be migrated:

    • Those whose GitHub accounts were deleted or whose logins can't be resolved
    • Provide a manual account recovery process through support

This approach eliminates the security risks, race conditions, and complexity of runtime migration while properly addressing the compatibility concerns raised by seefs001.

🤖 Fix all issues with AI agents
In @controller/github.go:
- Line 168: The assignment user.Username =
"github_"+strconv.Itoa(model.GetMaxUserId()+1) in the GitHub OAuth flow is racy
because concurrent calls to model.GetMaxUserId() can produce duplicate
usernames; replace this with a safe strategy such as deriving the username from
a database-generated unique value (e.g., user.ID or a DB sequence) or using a
generator that appends a random/UUID suffix, or implement
retry-on-unique-violation logic around the create/insert operation (loop:
generate candidate username, try insert, if unique constraint error retry a
bounded number of times and log/fail if exhausted); update the code path that
sets user.Username and the create/insert logic to use one of these
atomic/transactional approaches instead of GetMaxUserId().
- Around line 224-228: The duplicate-check incorrectly rejects binds when either
the numeric ID or the old login string is taken; change the logic so only the
immutable numeric ID is checked: replace the isAlreadyBound assignment that
calls model.IsGitHubIdAlreadyTaken(githubUser.Login) with a single check using
numericId (i.e., isAlreadyBound := model.IsGitHubIdAlreadyTaken(numericId)), and
remove or adjust any code paths, messages or variables that assumed the
login-based check (e.g., any branches using githubUser.Login) so binding is
allowed when numericId is free even if the old login string exists.
- Around line 113-129: The runtime fallback that checks githubUser.Login (the
model.IsGitHubIdAlreadyTaken(...) branch that sets user.GitHubId to
githubUser.Login and calls common.SysLog) creates an account-takeover risk;
remove that fallback and any in-code migration/update logic so the controller
only looks up users by immutable numericId (numericId and
model.IsGitHubIdAlreadyTaken(numericId)) and then calls FillUserByGitHubId with
the numeric ID; migrate existing login-based records with an offline DB
migration script prior to deployment. If you must keep runtime migration, only
allow a login-based match after verifying the stored numeric ID equals
githubUser.Id (store/compare the numeric ID) before updating user.GitHubId,
otherwise reject the match.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ce1074 and 2c54162.

📒 Files selected for processing (1)
  • controller/github.go
🔇 Additional comments (3)
controller/github.go (3)

26-26: LGTM: Added numeric ID field.

The addition of the Id field correctly captures GitHub's immutable numeric identifier, which is the right approach for stable user identification.


77-77: LGTM: Updated validation to use numeric ID.

The validation correctly checks for the presence of the numeric ID field.


244-244: LGTM: Correctly uses numeric ID for binding.

The bind operation properly stores the numeric ID as a string, which is the correct approach for new bindings.

Comment thread controller/github.go Outdated
Comment thread controller/github.go Outdated
Comment thread controller/github.go Outdated
Comment thread controller/github.go Outdated
@kosmoli
kosmoli force-pushed the fix/github-oauth-use-numeric-id branch from 2c54162 to 1573853 Compare January 8, 2026 14:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
controller/github.go (2)

176-182: Bug: Wrong error variable used in error response.

Line 179 uses err.Error() but err was set from getGitHubUserInfoByCode at line 109. The actual insertion error is in insertErr, so this will display the wrong error message to users.

🐛 Proposed fix
 			if insertErr != nil {
 				c.JSON(http.StatusOK, gin.H{
 					"success": false,
-					"message": err.Error(),
+					"message": insertErr.Error(),
 				})
 				return
 			}

228-237: Compilation error: user variable is undefined.

The user variable is referenced on lines 231, 232, 237, and 238, but it is never declared in the GitHubBind function. This code will not compile.

🐛 Proposed fix: Add user variable declaration
 	session := sessions.Default(c)
 	id := session.Get("id")
 	// id := c.GetInt("id")  // critical bug!
+	user := model.User{}
 	user.Id = id.(int)
 	err = user.FillUserById()
🤖 Fix all issues with AI agents
In @controller/github.go:
- Around line 269-282: The isDuplicateKeyError function is too broad (matching
"constraint") and can misclassify non-duplicate errors; update
isDuplicateKeyError to detect duplicate-key errors more precisely by checking
concrete indicators: for MySQL detect error number 1062 (or
mysql.MySQLError.Number == 1062), for PostgreSQL detect SQLSTATE "23505" or
pq.Error.Code == "23505", for SQLite match the exact message "UNIQUE constraint
failed" or sqlite3.ErrConstraintUnique, and keep explicit string matches like
"duplicate entry" and "duplicate key value violates unique constraint"; remove
the generic "constraint" substring check and prefer driver-specific error
type/code checks where possible (reference function isDuplicateKeyError).

In @docs/migrate_github_ids.go:
- Around line 154-167: The counting logic uses result[0] which indexes bytes not
runes, so replace the byte-based checks with Unicode-safe checks (e.g., use
strings.HasPrefix(result, "✓ ") / strings.HasPrefix(result, "✓") and
strings.HasPrefix(result, "✗") or parse the first rune via []rune(result) ) to
correctly detect success/skip/fail and increment successCount, skipCount, and
failCount accordingly; update the switch/if on result[0] to operate on the first
rune or string prefix and keep the existing result loop and counter variables
(result, successCount, skipCount, failCount).
- Around line 21-44: getGitHubNumericId currently makes unauthenticated requests
and will hit GitHub's 60/hr rate limit; update it to accept/use an authenticated
request (read a token from env/config and add an Authorization: bearer <TOKEN>
header) and add rate-limit handling: check resp.StatusCode for 401/403 and
inspect X-RateLimit-Remaining and X-RateLimit-Reset headers to either back off
and retry with exponential backoff or surface a clear error; also consider
making the HTTP client injectable (pass an *http.Client or context) so callers
can control timeout/retries and add retry logic around getGitHubNumericId to
handle transient 429/403 due to rate limiting.
🧹 Nitpick comments (2)
docs/migrate_github_ids.go (2)

3-12: Missing strings import needed for proper string comparison.

If you adopt the strings.HasPrefix fix for result counting, add "strings" to imports.


50-54: Numeric detection heuristic is acceptable but could be more robust.

Checking only the first character works because GitHub usernames cannot start with digits, but a full numeric validation would be safer.

♻️ Optional: Use strconv for complete validation
 	// 跳过已经是数字ID的用户
-	if len(user.GitHubId) > 0 && user.GitHubId[0] >= '0' && user.GitHubId[0] <= '9' {
+	if _, err := strconv.ParseInt(user.GitHubId, 10, 64); err == nil {
 		results <- fmt.Sprintf("✓ 跳过: %s (ID: %d) - github_id已经是数字格式: %s", user.Username, user.Id, user.GitHubId)
 		return
 	}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c54162 and 1573853.

📒 Files selected for processing (3)
  • controller/github.go
  • docs/migrate_github_ids.go
  • docs/migrate_github_ids.sql
✅ Files skipped from review due to trivial changes (1)
  • docs/migrate_github_ids.sql
🧰 Additional context used
🧬 Code graph analysis (1)
docs/migrate_github_ids.go (2)
controller/github.go (1)
  • GitHubUser (26-31)
model/main.go (1)
  • DB (64-64)
🔇 Additional comments (5)
docs/migrate_github_ids.go (2)

125-143: Concurrency implementation looks correct.

The semaphore pattern properly limits concurrent API calls, the goroutine correctly captures the user by value, and the results channel is appropriately sized.


80-104: Good UX: dry-run mode and confirmation before destructive operation.

The confirmation prompt and clear warning about database modification is a good practice for migration tools.

controller/github.go (3)

26-31: Good change: Using immutable numeric ID for validation.

Switching from mutable Login to immutable Id for identity validation is the correct approach for security.

Also applies to: 78-81


142-174: Retry mechanism for username collision is appropriate.

The retry loop with 5 attempts and logging is a reasonable approach for handling race conditions during concurrent user registration.


113-119: Security improvement: Using only numeric ID prevents account hijacking.

Good security practice. However, this requires all existing users to be migrated first using the migration tool, or they won't be able to log in. Ensure the migration tool is run before deploying this change.

Confirm that the deployment plan includes running the migration tool before deploying this controller change.

Comment thread controller/github.go Outdated
Comment thread docs/migrate_github_ids.go Outdated
Comment thread docs/migrate_github_ids.go
@kosmoli
kosmoli force-pushed the fix/github-oauth-use-numeric-id branch from 1573853 to fb52a9b Compare January 8, 2026 14:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
controller/github.go (2)

176-182: Critical bug: Wrong error variable used in error message.

Line 179 uses err.Error() but err is from the earlier getGitHubUserInfoByCode call (which succeeded if we reached this point). The correct variable is insertErr. This will either show an incorrect message or cause a nil pointer panic.

Proposed fix
 			if insertErr != nil {
 				c.JSON(http.StatusOK, gin.H{
 					"success": false,
-					"message": err.Error(),
+					"message": insertErr.Error(),
 				})
 				return
 			}

228-232: Critical bug: user variable is used but not declared.

user.Id and user.FillUserById() are referenced, but user is never declared in GitHubBind. This will cause a compile error. A user variable declaration is missing.

Proposed fix
 	session := sessions.Default(c)
 	id := session.Get("id")
 	// id := c.GetInt("id")  // critical bug!
+	user := model.User{}
 	user.Id = id.(int)
 	err = user.FillUserById()
🤖 Fix all issues with AI agents
In @docs/migrate_github_ids.go:
- Around line 167-171: The current check only inspects user.GitHubId[0], which
can falsely treat values like "123abc" as numeric; update the logic that skips
processing (the block referencing user.GitHubId and writing to results) to
validate the entire string is numeric by attempting to parse user.GitHubId as an
integer (e.g., with strconv.ParseInt/strconv.Atoi) or by verifying all runes are
digits, and only skip when the parse/validation succeeds; keep the same results
message and return behavior when the value is confirmed numeric.
- Around line 86-87: The fetchGitHubUser method interpolates raw username into
the GitHub API path, enabling path traversal; sanitize by URL-encoding the
username (e.g., via url.PathEscape) or validate it against an allowed username
pattern before building the URL, update fetchGitHubUser to use the
encoded/validated value when formatting the URL, and add "net/url" to the
imports.

In @docs/migrate_github_ids.sql:
- Around line 63-69: The UPDATE uses a self-referencing subquery on the users
table which can fail on some MySQL versions; change the update to avoid
referencing users in a subquery by either (a) performing a two-step update
(SELECT the target id first, then UPDATE by id), or (b) rewrite the statement to
use a derived filter/CTE or a direct WHERE clause (e.g., match github_id =
'alice' with deleted_at IS NULL and LIMIT 1) so the UPDATE on the users table
does not contain a subquery that reads the same table.
- Around line 91-104: The current SELECT uses MySQL-only REGEXP in the CASE
expressing migration_status (REGEXP '^[0-9]+$' on github_id), which breaks
Postgres/SQLite; update the docs to show database-specific alternatives instead
of a single REGEXP expression: for Postgres use the ~ operator on github_id, and
for SQLite use a combination of GLOB checks (e.g., numeric-only pattern and a
negative match for non-digits), and include these alternative CASE expressions
alongside the original so the users can run the same users -> github_id ->
migration_status query on MySQL, PostgreSQL, and SQLite.
🧹 Nitpick comments (4)
docs/migrate_github_ids.go (4)

17-21: GitHubUser struct is duplicated and missing the Email field.

This struct duplicates controller/github.go:26-31 but omits Email. Consider importing or referencing the canonical definition to avoid drift.


201-224: tokenFromFile is set but never used.

The variable is assigned on lines 216-217 and 221-222 but has no subsequent usage. Remove it or use it for logging/diagnostics.

Proposed fix
-	tokenFromFile := false
 	// ...
 	for i, arg := range os.Args {
 		if arg == "--token" && i+1 < len(os.Args) {
 			token = os.Args[i+1]
-			tokenFromFile = true
 			break
 		}
 		if strings.HasPrefix(arg, "--token=") {
 			token = strings.TrimPrefix(arg, "--token=")
-			tokenFromFile = true
 			break
 		}
 	}

316-329: Result counting logic using Unicode prefix matching is fragile and error-prone.

Distinguishing "success" ("✓ ") from "skip" ("✓" without trailing space) is brittle. A typo or formatting change will silently break the counts. Use a structured result type instead.

Proposed fix using a struct
type MigrationResult struct {
    Status  string // "success", "skip", "fail"
    Message string
}

// In migrateUser, return the struct via the channel instead of a formatted string.
// Then count by Status field for reliable tallying.

184-191: Redundant assignment: user.GitHubId is set twice.

Line 186 assigns user.GitHubId = newGitHubId, but the Update call on line 187 also explicitly sets "github_id", newGitHubId. One assignment suffices; remove the struct field mutation.

Proposed fix
 	if !dryRun {
 		// 更新数据库
-		user.GitHubId = newGitHubId
 		if err := model.DB.Model(&user).Update("github_id", newGitHubId).Error; err != nil {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1573853 and fb52a9b.

📒 Files selected for processing (3)
  • controller/github.go
  • docs/migrate_github_ids.go
  • docs/migrate_github_ids.sql
🧰 Additional context used
🧬 Code graph analysis (2)
docs/migrate_github_ids.go (2)
controller/github.go (1)
  • GitHubUser (26-31)
model/main.go (1)
  • DB (64-64)
controller/github.go (3)
model/user.go (4)
  • User (20-50)
  • IsGitHubIdAlreadyTaken (586-588)
  • GetMaxUserId (182-186)
  • GetUserIdByAffCode (303-310)
common/constants.go (2)
  • RoleCommonUser (138-138)
  • UserStatusEnabled (179-179)
common/sys_log.go (1)
  • SysLog (11-14)
🔇 Additional comments (4)
docs/migrate_github_ids.sql (1)

1-142: Good documentation coverage for the migration process.

The guide includes backup instructions, verification queries, rollback steps, and important notes about timing and idempotence. This is well-structured for operators.

controller/github.go (3)

26-31: LGTM: Added immutable numeric ID field to GitHubUser.

Using Id int64 from GitHub's API as the canonical identifier is the correct approach—it never changes even if users rename their accounts.


269-300: Good: Database-agnostic duplicate key detection.

The isDuplicateKeyError helper correctly handles MySQL, PostgreSQL, and SQLite error patterns. This enables the retry logic to work across database backends.


113-140: Security improvement: Using numeric ID prevents account hijacking.

The switch from mutable Login to immutable Id closes the vulnerability where a malicious user could rename their GitHub account to a previously-used username and hijack an existing account. Well done.

Comment thread controller/github.go Outdated
Comment thread docs/migrate_github_ids.go Outdated
Comment thread docs/migrate_github_ids.go Outdated
Comment thread docs/migrate_github_ids.sql
Comment thread docs/migrate_github_ids.sql
问题:
- GitHub OAuth使用可变的login字段作为唯一标识
- 当用户修改GitHub用户名后,会导致无法登录原账号
- 可能创建重复账号,用户失去原有数据和额度

解决方案:
- 在GitHubUser结构体添加Id字段(int64)
- 使用GitHub API返回的数字ID作为唯一标识
- 修改所有使用login的地方改用数字ID

修改内容:
1. controller/github.go:26 - 添加Id字段
2. controller/github.go:77 - 验证逻辑检查Id而非Login
3. controller/github.go:113 - GitHubOAuth函数使用数字ID
4. controller/github.go:192 - GitHubBind函数检查数字ID
5. controller/github.go:210 - GitHubBind更新逻辑使用数字ID

参考其他OAuth实现:
- Discord使用UID (数字ID)
- OIDC使用sub (不可变标识符)
- Telegram使用数字ID
- 现GitHub也统一使用数字ID

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@kosmoli
kosmoli force-pushed the fix/github-oauth-use-numeric-id branch from fb52a9b to 76fde98 Compare January 8, 2026 14:51
@seefs001

seefs001 commented Jan 8, 2026

Copy link
Copy Markdown
Collaborator

建议你还是先别弄了,你都被CodeRabbit和你的Claude Code带偏到哪儿去了

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
controller/github.go (1)

176-182: Critical bug: Wrong error variable returned.

Line 179 returns err.Error() but should return insertErr.Error(). The variable err is from the earlier GitHub API call (line 109), while insertErr contains the actual insertion error. This will either return the wrong error message or cause a nil pointer dereference if err is nil.

🐛 Fix the error variable
 		if insertErr != nil {
 			c.JSON(http.StatusOK, gin.H{
 				"success": false,
-				"message": err.Error(),
+				"message": insertErr.Error(),
 			})
 			return
 		}
🤖 Fix all issues with AI agents
In @controller/github.go:
- Around line 142-174: The current username generation uses
model.GetMaxUserId()+1 (in the loop constructing user.Username) which causes a
race under concurrent OAuth logins; replace this with a deterministic unique
suffix such as the GitHub numeric ID or a timestamp/UUID to guarantee uniqueness
(e.g., set user.Username to "github_"+<github numeric id> or a
timestamp+random/UUID), update the code that reads githubUser to use its numeric
ID field when building user.Username, and remove reliance on
model.GetMaxUserId()/retry for uniqueness so user.Insert (and the
isDuplicateKeyError check) no longer depends on fragile max-id logic.
- Around line 270-300: The current string-matching duplicate-key detection is
fragile; enable GORM's TranslateError on all gorm.Config instances used to open
DB connections (set TranslateError: true alongside existing PrepareStmt: true)
and replace the isDuplicateKeyError implementation to use type-safe checking via
errors.Is(err, gorm.ErrDuplicatedKey) instead of string inspection; update
imports accordingly so the function uses the standard errors package and
gorm.ErrDuplicatedKey and remove the old string checks.
🧹 Nitpick comments (2)
docs/migrate_github_ids.go (2)

292-310: Consider reducing the results channel buffer size.

Line 296 allocates a buffer equal to the number of users, which could consume significant memory for large user counts (e.g., 10,000+ users). Since the goroutines are waited on before reading results (line 309), consider using an unbuffered channel or a smaller fixed buffer.

♻️ Suggested refactor
-	results := make(chan string, len(users))
+	results := make(chan string, 100)  // Fixed smaller buffer

Or use an unbuffered channel:

-	results := make(chan string, len(users))
+	results := make(chan string)

321-334: Fragile result counting logic.

The string-prefix matching for Unicode symbols (lines 324-333) is brittle and assumes exact formatting. The distinction between "✓ " (with space, line 325) meaning success and "✓" alone (line 328) meaning skip is error-prone.

♻️ More robust approach

Consider using structured result types instead of string formatting:

type MigrationResult struct {
	Status  string  // "success", "skip", "fail"
	Message string
}

// Then modify migrateUser to send MigrationResult instead of string
// and count based on result.Status instead of string prefix parsing
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb52a9b and 76fde98.

📒 Files selected for processing (3)
  • controller/github.go
  • docs/migrate_github_ids.go
  • docs/migrate_github_ids.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/migrate_github_ids.sql
🧰 Additional context used
🧬 Code graph analysis (2)
docs/migrate_github_ids.go (2)
controller/github.go (1)
  • GitHubUser (26-31)
model/main.go (1)
  • DB (64-64)
controller/github.go (3)
model/user.go (4)
  • User (20-50)
  • IsGitHubIdAlreadyTaken (586-588)
  • GetMaxUserId (182-186)
  • GetUserIdByAffCode (303-310)
common/constants.go (2)
  • RoleCommonUser (138-138)
  • UserStatusEnabled (179-179)
common/sys_log.go (1)
  • SysLog (11-14)
🔇 Additional comments (8)
docs/migrate_github_ids.go (5)

24-40: LGTM: Clean API client design.

The client initialization is well-structured with a reasonable 30s timeout and descriptive user agent.


42-84: LGTM: Robust retry mechanism.

The exponential backoff with rate-limit handling and appropriate retry conditions (skipping 404s) follows best practices for GitHub API interaction.


86-144: LGTM: Secure API interaction.

The implementation properly escapes the username (line 89), handles authentication, and covers all relevant HTTP status codes.


146-164: LGTM: Useful rate-limit monitoring.

The rate-limit checks provide helpful visibility during migration without blocking execution.


166-200: LGTM: Clean migration logic per user.

The function correctly skips already-migrated users (line 171), handles both dry-run and live modes, and reports results appropriately.

controller/github.go (3)

26-31: LGTM: Correctly adds numeric ID field.

The Id int64 field enables using GitHub's immutable numeric identifier instead of the mutable login name.


78-80: LGTM: Validation switched to immutable ID.

Using Id == 0 as the validation check correctly leverages the immutable numeric identifier.


216-237: LGTM: Bind logic correctly uses numeric IDs.

The binding flow consistently uses numericId for duplicate checks and assignment, preventing account hijacking via username changes.

Comment thread controller/github.go Outdated
Comment thread controller/github.go Outdated
Comment on lines +270 to +300
// isDuplicateKeyError 检查错误是否是数据库唯一约束冲突
// 通过检测数据库特定的错误码或消息来精确识别
func isDuplicateKeyError(err error) bool {
if err == nil {
return false
}

errMsg := strings.ToLower(err.Error())

// MySQL: Error 1062: Duplicate entry
// 检查 "Duplicate entry" 或 "ER_DUP_ENTRY"
if strings.Contains(errMsg, "duplicate entry") || strings.Contains(errMsg, "er_dup_entry") {
return true
}

// PostgreSQL: SQLSTATE 23505 - unique_violation
// 检查 "duplicate key value violates unique constraint" 或 "23505"
if strings.Contains(errMsg, "duplicate key value violates unique constraint") ||
strings.Contains(errMsg, "sqlstate 23505") ||
strings.Contains(errMsg, "23505") {
return true
}

// SQLite: "UNIQUE constraint failed"
// 精确匹配 "UNIQUE constraint failed"
if strings.Contains(errMsg, "unique constraint failed") {
return true
}

return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the function in controller/github.go
cat -n controller/github.go | sed -n '270,300p'

Repository: QuantumNous/new-api

Length of output: 1127


🏁 Script executed:

# Search for where isDuplicateKeyError is used
rg -n "isDuplicateKeyError" -A 3 -B 3

Repository: QuantumNous/new-api

Length of output: 784


🏁 Script executed:

# Look for test cases related to duplicate key error detection
rg -n "isDuplicateKeyError|duplicate.*key|UNIQUE.*constraint" -t go

Repository: QuantumNous/new-api

Length of output: 604


🏁 Script executed:

# Search for GORM error handling patterns
rg -n "gorm|errors\.Is|errors\.As" controller/github.go -B 2 -A 2

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check if there's any GORM-specific error type handling
rg -n "gorm\..*[Ee]rror" -t go

Repository: QuantumNous/new-api

Length of output: 2223


🏁 Script executed:

# Find User.Insert implementation
rg -n "func.*Insert" -A 10 model/user.go | head -50

Repository: QuantumNous/new-api

Length of output: 413


🏁 Script executed:

# Look for how errors are handled in inserts
rg -n "Insert|insert" controller/github.go -B 5 -A 5

Repository: QuantumNous/new-api

Length of output: 1068


🏁 Script executed:

# Check if there are any database/sql or driver error handling patterns
rg -n "database/sql|driver\." -t go | head -20

Repository: QuantumNous/new-api

Length of output: 612


🏁 Script executed:

# Search for any existing error wrapping or error type checking
rg -n "errors\.Is|errors\.As|errors\.Unwrap" -t go

Repository: QuantumNous/new-api

Length of output: 2202


🏁 Script executed:

# Check GORM version and documentation context
fd -t f "go.mod|go.sum" -x cat

Repository: QuantumNous/new-api

Length of output: 40223


🏁 Script executed:

# Check if there are any GORM-specific error types we can use
rg -n "gorm\.Err|clause\.|Driver" -t go | head -20

Repository: QuantumNous/new-api

Length of output: 844


🌐 Web query:

GORM v1.25.2 duplicate key error handling type-safe approach

💡 Result:

Short answer — two safe options:

  1. Use GORM’s translator (recommended for DB-agnostic code)
  • Enable TranslateError when opening DB so GORM maps driver errors to gorm.ErrDuplicatedKey:
    db, _ := gorm.Open(dialector, &gorm.Config{ TranslateError: true })
  • Then check:
    if err != nil && errors.Is(err, gorm.ErrDuplicatedKey) { /* handle duplicate */ }
    Reference: GORM error-handling / TranslateError. [1]
  1. Driver-specific, type-safe check (Postgres example)
  • Inspect the concrete error with errors.As and compare the SQLSTATE (23505) via pgconn/pgerrcode:
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) && pgErr.Code == pgerrcode.UniqueViolation {
    // handle duplicate (23505)
    }
    Notes: import the exact pgconn package used by your driver (e.g. github.com/jackc/pgx/v5/pgconn) to make errors.As succeed. [2][3]

Context: v1.25.x introduced gorm.ErrDuplicatedKey and the TranslateError option — without TranslateError the ErrDuplicatedKey mapping will not be produced, so errors.Is will not match. Use option (1) for a portable approach; use option (2) when you need exact DB-driver details. [2][1][3]


🌐 Web query:

GORM database/sql driver error wrapping PostgreSQL MySQL SQLite unique constraint

💡 Result:

Short answer — database drivers wrap errors, so use errors.As to unwrap and check driver-specific codes (or enable GORM's TranslateError to get gorm.ErrDuplicatedKey).

Examples

  • GORM TranslateError (unified): set gorm.Config{TranslateError: true} and check errors.Is(err, gorm.ErrDuplicatedKey). [1]

  • PostgreSQL (lib/pq or pgconn): unwrap to *pq.Error or *pgconn.PgError and compare Code == "23505" (unique_violation). Use errors.As to handle wrapped errors. [2][3]

    Example:
    var pgErr pq.Error
    if errors.As(err, &pgErr) && pgErr.Code == "23505" { /
    duplicate */ }

  • MySQL (go-sql-driver/mysql): unwrap to *mysql.MySQLError and check Number == 1062 (ER_DUP_ENTRY). [4]

    Example:
    var myErr mysql.MySQLError
    if errors.As(err, &myErr) && myErr.Number == 1062 { /
    duplicate */ }

  • SQLite (mattn/go-sqlite3): driver exposes sqlite3.ErrConstraint and extended result codes; unwrap and inspect the driver error (or use errors.Is(err, sqlite3.ErrConstraint)). The sqlite3 driver/SQL returns constraint errors for UNIQUE violations. [5]

Notes

  • Always use errors.As (not string matching) because drivers (and GORM) commonly wrap errors. [2][4]
  • If you want DB-agnostic handling, enable GORM TranslateError and test gorm.ErrDuplicatedKey. [1]

Sources
[1] GORM error handling / TranslateError docs.
[2] lib/pq / Postgres unique_violation (SQLSTATE 23505) examples.
[3] Discussion/examples using pgconn/pq with errors.As (StackOverflow/blog).
[4] go-sql-driver/mysql duplicate-entry handling (error Number 1062).
[5] mattn/go-sqlite3 driver README / constraint error behavior.


🏁 Script executed:

# Find database initialization and configuration
rg -n "gorm.Open|gorm.Config" -A 5 -B 2 model/main.go | head -40

Repository: QuantumNous/new-api

Length of output: 1215


🏁 Script executed:

# Look for TranslateError setting
rg -n "TranslateError" -i

Repository: QuantumNous/new-api

Length of output: 45


Enable GORM's TranslateError and use type-safe error checking instead of string matching.

The string-based approach is fragile and will break if database drivers change error messages across versions. GORM v1.25.2 provides gorm.ErrDuplicatedKey, but it requires enabling TranslateError in the database configuration.

To fix this:

  1. Enable TranslateError: true in gorm.Config for all database connections in model/main.go (lines 135, 146, 165, 172):
&gorm.Config{
    PrepareStmt: true,
    TranslateError: true,  // Add this
}
  1. Replace the isDuplicateKeyError function with:
func isDuplicateKeyError(err error) bool {
    return errors.Is(err, gorm.ErrDuplicatedKey)
}

This aligns with the codebase's existing error handling pattern (used with gorm.ErrRecordNotFound throughout) and eliminates fragility from string matching.

🤖 Prompt for AI Agents
In @controller/github.go around lines 270 - 300, The current string-matching
duplicate-key detection is fragile; enable GORM's TranslateError on all
gorm.Config instances used to open DB connections (set TranslateError: true
alongside existing PrepareStmt: true) and replace the isDuplicateKeyError
implementation to use type-safe checking via errors.Is(err,
gorm.ErrDuplicatedKey) instead of string inspection; update imports accordingly
so the function uses the standard errors package and gorm.ErrDuplicatedKey and
remove the old string checks.

Remove the retry mechanism in username generation since we now use GitHub numeric ID directly as the username suffix, which eliminates race conditions. Also remove the isDuplicateKeyError function as it's no longer needed.

Changes:
- controller/github.go:
  - Remove isDuplicateKeyError function (no longer needed)
  - Remove unused imports (strings, gorm.io/gorm)
  - Fix missing user variable declaration in GitHubBind function

- docs/migrate_github_ids.go:
  - Remove unused tokenFromFile variable
  - Change common.Setup() to model.InitDB()
  - Remove unused common import

The username generation now directly uses GitHub numeric ID as suffix:
  user.Username = "github_" + numericId

This is deterministic, race-condition-free, and doesn't require retry logic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
controller/github.go (2)

199-209: Duplicate binding risk: Ensure GitHub ID migration is completed before deployment.

The binding check validates only against numeric IDs. If a GitHub account was previously bound using login-based format (from before the numeric ID requirement), IsGitHubIdAlreadyTaken(numericId) will not detect the existing binding, allowing the same GitHub account to be bound to multiple user accounts.

The migration script (docs/migrate_github_ids.go) exists to convert old login-based GitHub IDs to numeric format, but it is a manual, optional tool—not an automatic migration. If not executed before deploying this code, old bindings remain undetected and the duplicate binding vulnerability persists.

Required action: Run the migration script to convert all existing github_id values to numeric format before deploying this change.


113-172: Critical: Incomplete safeguards for GitHub ID migration — existing users will lose login access if migration is skipped.

While migration tools exist (docs/migrate_github_ids.go and docs/migrate_github_ids.sql), they are entirely optional and manual. The code will immediately break login for any users with non-numeric github_id values:

  1. Existing users have github_id as login names (e.g., "alice")
  2. New code only checks for numeric IDs via IsGitHubIdAlreadyTaken(numericId)
  3. Without migration, the lookup fails and the system attempts to create a duplicate account
  4. Result: Login failures for all existing GitHub OAuth users

The vulnerability: Migration is not enforced or validated at startup. There is no:

  • Deployment check to prevent starting without migration
  • Startup validation to warn about unmigrated users
  • Documentation in README requiring migration as a prerequisite

This creates a critical deployment risk—a forgotten migration will silently break production login without triggering any warnings.

Required fixes:

  • Add startup validation to detect and reject deployment if unmigrated users exist, OR
  • Add explicit documentation in deployment guide marking migration as mandatory before deploying this code, AND
  • Reference the migration tools in prominent deployment prerequisites
🧹 Nitpick comments (3)
controller/github.go (3)

138-139: Remove redundant assignment.

user.GitHubId = numericId is already set on line 115. This assignment on line 139 is redundant.

♻️ Remove redundant assignment
-		// 创建新账号,使用数字ID作为GitHub ID
-		user.GitHubId = numericId
-
 		// 使用GitHub数字ID作为用户名后缀,确保唯一性且无竞态条件

141-143: Consider more user-friendly username generation.

While "github_" + numericId is deterministic and avoids race conditions, usernames like github_12345678 are not user-friendly. Consider either:

  1. Use GitHub login with numeric ID fallback: githubUser.Login (or "github_" + numericId if login is unavailable/taken), OR
  2. Allow users to customize their username after first login, OR
  3. Document that users should update their username via account settings after registration

The current approach is functional but creates poor initial UX.


83-231: Deployment requires strict sequencing to prevent user lockout.

This is a breaking change that fundamentally alters the github_id storage format. To deploy safely:

  1. Pre-deployment: Run the migration scripts (docs/migrate_github_ids.go and SQL migrations) to convert ALL existing login-based github_id values to numeric IDs
  2. Verify: Confirm zero users remain with non-numeric github_id values
  3. Deploy: Only after successful migration, deploy this application code
  4. Monitor: Watch for login failures immediately after deployment

Missing safeguard: Consider adding a startup validation check that scans for any remaining non-numeric github_id values and refuses to start if found. This prevents accidental deployment before migration is complete.

Example validation:

// On application startup
func ValidateGitHubIdMigration() error {
    var count int64
    // Check for any github_id that doesn't parse as a valid int64
    // This would indicate unmigrated login-based IDs
    DB.Model(&User{}).Where("github_id != '' AND github_id NOT REGEXP '^[0-9]+$'").Count(&count)
    if count > 0 {
        return fmt.Errorf("Found %d users with unmigrated github_id values. Run migration first.", count)
    }
    return nil
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 76fde98 and 50b85cf.

📒 Files selected for processing (2)
  • controller/github.go
  • docs/migrate_github_ids.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/migrate_github_ids.go
🧰 Additional context used
🧬 Code graph analysis (1)
controller/github.go (2)
model/user.go (2)
  • User (20-50)
  • IsGitHubIdAlreadyTaken (586-588)
common/gin.go (1)
  • ApiError (141-146)
🔇 Additional comments (3)
controller/github.go (3)

26-26: Good addition of immutable GitHub ID field.

Adding the numeric Id field is the correct approach, as GitHub's numeric IDs are immutable and prevent account hijacking risks associated with mutable login names.


77-79: Validation correctly updated to check numeric ID.

The change from validating Login to validating Id == 0 is appropriate for ensuring GitHub's numeric identifier is present.


220-220: Correct assignment of numeric ID for binding.

The assignment correctly stores GitHub's numeric ID when binding accounts.

@kosmoli

kosmoli commented Jan 8, 2026

Copy link
Copy Markdown
Author

建议你还是先别弄了,你都被CodeRabbit和你的Claude Code带偏到哪儿去了

唉……好吧……

@Calcium-Ion

Copy link
Copy Markdown
Member

感谢您的贡献,这个确实是一个比较严重的问题,我们看了一下pr,代码实现的方式不是很符合我们的想法,后面我们会开一个新的pr,到时候把您设置为合作贡献者,您看可以吗

@kosmoli

kosmoli commented Jan 8, 2026

Copy link
Copy Markdown
Author

感谢您的贡献,这个确实是一个比较严重的问题,我们看了一下pr,代码实现的方式不是很符合我们的想法,后面我们会开一个新的pr,到时候把您设置为合作贡献者,您看可以吗

好的,谢谢您

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.