Skip to content

Conversation

@nodeloc
Copy link

@nodeloc nodeloc commented Jan 20, 2026

Summary

新增 NodeLoc OAuth 登录支持,允许用户通过 NodeLoc 账号直接登录或注册。

Changes

Backend

  • 新增 controller/nodeloc.go - NodeLoc OAuth 核心逻辑
  • model/user.go - 新增 nodeloc_id 字段及相关查询方法
  • model/option.go - 新增 NodeLoc 相关配置项
  • controller/misc.go - 在 status API 中返回 NodeLoc OAuth 状态
  • controller/option.go - 新增 NodeLoc 配置项处理
  • router/api-router.go - 新增 NodeLoc OAuth 路由
  • common/constants.go - 新增 NodeLoc 相关常量

Frontend

  • web/src/components/auth/LoginForm.jsx - 登录页添加 NodeLoc 登录按钮
  • web/src/components/auth/RegisterForm.jsx - 注册页添加 NodeLoc 登录按钮
  • web/src/components/auth/OAuth2Callback.jsx - 优化 OAuth 回调处理,业务错误不再重试
  • web/src/components/settings/SystemSetting.jsx - 系统设置中添加 NodeLoc OAuth 配置
  • web/src/components/settings/personal/cards/AccountManagement.jsx - 个人设置中支持绑定/解绑 NodeLoc
  • web/src/components/common/logo/NodeLocIcon.jsx - 新增 NodeLoc 图标组件
  • web/src/helpers/api.js - 新增 onNodeLocOAuthClicked 方法
  • web/src/App.jsx - 新增 /oauth/nodeloc 路由

Documentation

  • 新增 docs/nodeloc-oauth.md - NodeLoc OAuth 配置文档

Features

  • 支持通过 NodeLoc 账号登录
  • 支持新用户通过 NodeLoc OAuth 自动注册
  • 支持已有用户绑定/解绑 NodeLoc 账号
  • 支持配置最低信任等级要求
  • 用户名优先使用 NodeLoc 用户名,重复时自动添加后缀
  • 已删除用户可重新通过 OAuth 注册

Configuration

在系统设置中配置以下选项:

  • NodeLoc Client ID
  • NodeLoc Client Secret
  • NodeLoc 最低信任等级(可选)

Test Plan

  • 新用户通过 NodeLoc 登录自动注册
  • 已有用户通过 NodeLoc 登录
  • 已登录用户绑定 NodeLoc 账号
  • 解绑 NodeLoc 账号
  • 信任等级不足时显示错误提示
  • 用户名重复时自动添加后缀
  • 已删除用户重新注册

Summary by CodeRabbit

Release Notes - v1.0.3

  • New Features

    • NodeLoc OAuth integration: login, optional auto-registration with trust checks, and account binding.
    • UI additions: NodeLoc login buttons, OAuth callback route, account binding card, and system settings panel to configure NodeLoc credentials and minimum trust level.
  • Documentation

    • Added a full NodeLoc OAuth integration guide.
  • Chores

    • Version bumped to v1.0.3 and updated CI image references.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

Walkthrough

Adds NodeLoc OAuth2 integration: new backend handlers and user model fields, frontend OAuth/UI/routes, system settings, env constants, docs, and CI image name updates; includes token/user endpoint config and version bump.

Changes

Cohort / File(s) Summary
Env & Version
\.env.example`, `VERSION``
Adds NodeLoc endpoint env vars (NODELOC_TOKEN_ENDPOINT, NODELOC_USER_ENDPOINT) and bumps VERSION to 1.0.3.
Constants & Options
\common/constants.go`, `model/option.go`, `controller/option.go``
Adds NodeLoc config flags (NodeLocOAuthEnabled, NodeLocClientId, NodeLocClientSecret, NodeLocMinimumTrustLevel), option registration and enablement validation.
Backend: OAuth & API
\controller/nodeloc.go`, `controller/misc.go`, `controller/user.go`, `router/api-router.go``
Implements NodeLoc OAuth handlers (NodeLocOAuth, NodeLocBind), helper to exchange code and fetch userinfo, exposes NodeLoc fields in status/self endpoints, and registers GET /oauth/nodeloc.
Model Changes
\model/user.go`, `model/option.go``
Adds User.NodeLocId (DB index) and helpers IsUsernameExist, IsNodeLocIdAlreadyTaken, FillUserByNodeLocId; option map extended for NodeLoc keys.
Frontend: Routing & Helpers
\web/src/App.jsx`, `web/src/helpers/api.js``
Adds /oauth/nodeloc route and onNodeLocOAuthClicked client helper to prepare state and redirect to NodeLoc authorize endpoint.
Frontend: Auth UIs
\web/src/components/auth/LoginForm.jsx`, `web/src/components/auth/RegisterForm.jsx`, `web/src/components/auth/OAuth2Callback.jsx``
Adds NodeLoc OAuth buttons/handlers and loading state; improves OAuth callback error handling and retry-on-network behavior, redirects to login on final failure.
Frontend: Settings & Account
\web/src/components/settings/SystemSetting.jsx`, `web/src/components/settings/personal/cards/AccountManagement.jsx`, `web/src/components/common/logo/NodeLocIcon.jsx``
Adds NodeLoc OAuth config UI (client id/secret/trust level), enable checkbox, account binding card, and a new NodeLocIcon component (SVG).
Docs
\docs/nodeloc-oauth.md``
New documentation covering NodeLoc OAuth flow, configuration, DB changes, security notes and implementation details.
Build & CI
\web/vite.config.js`, .github/workflows/docker-image-*.yml`
Adds Vite alias for semi-ui CSS; updates CI Docker image name references from calciumion/new-api to nodeloc/new-api.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend
    participant Backend
    participant NodeLoc
    participant Database

    User->>Frontend: Click NodeLoc OAuth button
    Frontend->>Frontend: prepare state (consent check)
    Frontend->>NodeLoc: Redirect to /oauth/authorize (client_id, state, redirect_uri)
    NodeLoc->>User: Prompt consent
    User->>NodeLoc: Approve
    NodeLoc->>Frontend: Redirect to /oauth/nodeloc?code=&state=
    Frontend->>Backend: GET /oauth/nodeloc?code=&state=
    Backend->>Backend: validate state
    Backend->>NodeLoc: POST /oauth-provider/token (code, client_secret)
    NodeLoc-->>Backend: access_token
    Backend->>NodeLoc: GET /oauth-provider/userinfo (access_token)
    NodeLoc-->>Backend: user info (id, username, trust_level,...)
    alt binding (user session exists)
        Backend->>Database: update user.NodeLocId
    else login/registration
        Backend->>Database: find by NodeLocId or create user (ensure username uniqueness, check trust level)
    end
    Backend->>Backend: setupLogin/session
    Backend-->>Frontend: redirect to token/success
    Frontend->>Frontend: store token & navigate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • Calcium-Ion

Poem

🐰 I hopped a token, swift and spry,
I fetched a user from the sky,
Bound accounts and opened the gate,
Green and orange, login's great,
NodeLoc now helps users fly ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding NodeLoc OAuth login support, which aligns with the comprehensive backend, frontend, and documentation changes throughout the changeset.

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


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (4)
model/option.go (1)

32-44: Add missing NodeLoc (and LinuxDO) credential defaults to InitOptionMap.

NodeLocClientId, NodeLocClientSecret, and NodeLocMinimumTrustLevel keys are not initialized in InitOptionMap, even though NodeLocOAuthEnabled is seeded at line 42. This causes these keys to be absent from the options API until the first database save. The same issue affects LinuxDO credentials. Follow the existing pattern used for GitHub, Telegram, and WeChat by adding empty string/default value initializations:

 common.OptionMap["GitHubClientId"] = ""
 common.OptionMap["GitHubClientSecret"] = ""
+common.OptionMap["LinuxDOClientId"] = ""
+common.OptionMap["LinuxDOClientSecret"] = ""
+common.OptionMap["LinuxDOMinimumTrustLevel"] = strconv.Itoa(common.LinuxDOMinimumTrustLevel)
+common.OptionMap["NodeLocClientId"] = ""
+common.OptionMap["NodeLocClientSecret"] = ""
+common.OptionMap["NodeLocMinimumTrustLevel"] = strconv.Itoa(common.NodeLocMinimumTrustLevel)
 common.OptionMap["TelegramBotToken"] = ""
web/src/components/auth/OAuth2Callback.jsx (1)

86-91: Inconsistent navigation target on missing authorization code.

When the authorization code is missing (line 88-90), the user is redirected to /console/personal, but other error scenarios (lines 53, 78) redirect to /login. This inconsistency could confuse users.

🔧 Suggested fix for consistent navigation
     // 参数缺失直接返回
     if (!code) {
       showError(t('未获取到授权码'));
-      navigate('/console/personal');
+      navigate('/login');
       return;
     }
web/src/components/auth/RegisterForm.jsx (1)

662-667: status.nodeloc_oauth missing from conditional.

The conditional that determines whether to show "other register options" doesn't include status.nodeloc_oauth. If NodeLoc OAuth is the only enabled OAuth provider, users won't see the button to switch back from email registration.

Proposed fix
 {(status.github_oauth ||
   status.discord_oauth ||
   status.oidc_enabled ||
   status.wechat_login ||
   status.linuxdo_oauth ||
+  status.nodeloc_oauth ||
   status.telegram_oauth) && (
web/src/components/auth/LoginForm.jsx (1)

797-802: status.nodeloc_oauth missing from conditional.

Similar to RegisterForm.jsx, the "other login options" conditional doesn't include status.nodeloc_oauth. This should be added for consistency with line 938.

Proposed fix
 {(status.github_oauth ||
   status.discord_oauth ||
   status.oidc_enabled ||
   status.wechat_login ||
   status.linuxdo_oauth ||
+  status.nodeloc_oauth ||
   status.telegram_oauth) && (
🤖 Fix all issues with AI agents
In `@controller/nodeloc.go`:
- Around line 178-185: The state validation currently uses an unsafe type
assertion on session.Get("oauth_state"); change it to perform a safe comma-ok
type assertion (e.g., val, ok := session.Get("oauth_state").(string)), verify ok
is true and val is non-empty before comparing to the query `state`, and if the
assertion fails or values mismatch return the same forbidden JSON response;
update the comparison in the handler where `state` and
`session.Get("oauth_state")` are used to reference the safely-asserted variable
(so no direct .(string) assertion remains).
- Around line 58-60: The code uses session := sessions.Default(c); id :=
session.Get("id"); user.Id = id.(int) which can panic if id is nil or not an
int; change this to guard the value by checking for nil and using a safe type
assertion (idInt, ok := id.(int)) and handle the failure (e.g., return an
error/abort the request, set a safe default, or log and respond) before
assigning to user.Id; update any related logic in the handler that assumes a
valid user.Id so it doesn't proceed on missing/invalid session id.
- Around line 233-236: The current username suffix logic using
model.IsUsernameExist and model.GetMaxUserId is racy because GetMaxUserId() and
the subsequent user.Insert are not atomic; replace this with an atomic
conflict-safe approach: enforce a unique DB constraint on username and implement
insert-with-conflict-retry (catch unique-violation on user.Insert and retry with
a new suffix), or derive the suffix from the NodeLoc account ID
(nodelocUser.Username + "_" + nodelocUser.ID) or a UUID/timestamp so uniqueness
is guaranteed; update the code paths that call model.IsUsernameExist,
model.GetMaxUserId, and user.Insert to remove the non-atomic check-and-create
and instead perform the conflict-handling retry or deterministic-suffix logic.
- Around line 142-151: The response from client.Do(req) in the NodeLoc user
lookup is not checked for HTTP success before decoding into NodeLocUser; update
the code in the function performing client.Do(req) to verify res2.StatusCode is
a 2xx success (e.g., == http.StatusOK) before calling
json.NewDecoder(res2.Body).Decode(&nodelocUser); if the status is non-2xx, read
the response body (or at least include res2.StatusCode) and return a descriptive
error that includes the status code and any error payload instead of attempting
to decode into NodeLocUser.

In `@docs/nodeloc-oauth.md`:
- Around line 15-18: Update the markdown snippets in docs/nodeloc-oauth.md to
use fenced code blocks with a language specifier (e.g., ```text) for the URL
examples and replace bare URLs in prose with angle-bracketed links (e.g.,
<https://...>) so they are not treated as plain bare URLs; specifically change
the inline blocks containing
"https://www.nodeloc.com/oauth-provider/applications" and the authorize URL
block (client_id/redirect_uri/response_type...) to ```text fences, and update
occurrences like "https://docs.nodeloc.com/api-reference/introduction" to use
<...> angle-bracket notation; apply the same changes to the other occurrences
noted (lines around 60-67 and 108-110).

In `@web/package.json`:
- Line 13: Remove the unused "antd" dependency from web/package.json (delete the
"antd": "^6.2.0" entry), then update the lockfile by running your package
manager install (npm/yarn/pnpm) so the lockfile is regenerated; also run a quick
project-wide search for any stray "antd" imports/usages to ensure nothing breaks
and run the build/tests to verify no references remain.

In `@web/src/components/common/logo/NodeLocIcon.jsx`:
- Around line 38-41: In the NodeLocIcon component update the SVG stop attributes
to JSX camelCase: replace the two occurrences of stop-color with stopColor
inside the <linearGradient id="paint0_linear_1_6"> block (the <stop> elements)
so the JSX uses stopColor="#009966" and stopColor="#FF9933" respectively.
🧹 Nitpick comments (12)
controller/misc.go (1)

59-73: Formatting issue degrades readability.

Multiple key-value pairs are concatenated on single lines (e.g., lines 59 and 61), which appears to be an unintended merge artifact. This makes the code harder to maintain and review.

♻️ Suggested formatting fix
-		"linuxdo_minimum_trust_level": common.LinuxDOMinimumTrustLevel, "nodeloc_oauth": common.NodeLocOAuthEnabled,
-		"nodeloc_client_id":           common.NodeLocClientId,
-		"nodeloc_minimum_trust_level": common.NodeLocMinimumTrustLevel, "telegram_oauth": common.TelegramOAuthEnabled,
-		"telegram_bot_name":  common.TelegramBotName,
+		"linuxdo_minimum_trust_level": common.LinuxDOMinimumTrustLevel,
+		"nodeloc_oauth":               common.NodeLocOAuthEnabled,
+		"nodeloc_client_id":           common.NodeLocClientId,
+		"nodeloc_minimum_trust_level": common.NodeLocMinimumTrustLevel,
+		"telegram_oauth":              common.TelegramOAuthEnabled,
+		"telegram_bot_name":           common.TelegramBotName,
controller/nodeloc.go (3)

107-112: Consider a longer timeout for external OAuth requests.

5-second timeout may be too aggressive for OAuth token exchange, especially under network latency. Consider 10-15 seconds.

-	client := http.Client{Timeout: 5 * time.Second}
+	client := http.Client{Timeout: 10 * time.Second}

81-84: Unused parameter c *gin.Context in helper function.

The getNodeLocUserInfoByCode function receives c *gin.Context but never uses it. Remove the unused parameter.

-func getNodeLocUserInfoByCode(code string, c *gin.Context) (*NodeLocUser, error) {
+func getNodeLocUserInfoByCode(code string) (*NodeLocUser, error) {

And update callers at lines 40 and 202:

-	nodelocUser, err := getNodeLocUserInfoByCode(code, c)
+	nodelocUser, err := getNodeLocUserInfoByCode(code)

242-246: Silent error swallowing for invalid affCode.

When GetUserIdByAffCode fails, the error is discarded with _. Consider logging the error for debugging purposes.

 				affCode := session.Get("aff")
 				inviterId := 0
 				if affCode != nil {
-					inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
+					var err error
+					inviterId, err = model.GetUserIdByAffCode(affCode.(string))
+					if err != nil {
+						common.SysLog(fmt.Sprintf("Invalid aff code during NodeLoc registration: %s", affCode))
+					}
 				}
web/src/components/settings/personal/cards/AccountManagement.jsx (1)

456-493: Consider using the NodeLocIcon component instead of inline SVG.

A NodeLocIcon component already exists in web/src/components/common/logo/NodeLocIcon.jsx. Using it would improve consistency and maintainability.

♻️ Proposed refactor

Add import:

+import NodeLocIcon from '../../../common/logo/NodeLocIcon';

Replace the inline SVG:

-                    <div className='w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3 flex-shrink-0'>
-                      <svg
-                        width='20'
-                        height='20'
-                        viewBox='0 0 24 24'
-                        className='text-blue-600 dark:text-blue-300'
-                      >
-                        <circle cx='12' cy='7' r='2.5' fill='currentColor' />
-                        ...
-                      </svg>
-                    </div>
+                    <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
+                      <NodeLocIcon size={20} />
+                    </div>

Note: Also consider using bg-slate-100 for visual consistency with other binding cards.

web/src/components/common/logo/NodeLocIcon.jsx (1)

23-49: CustomIcon is recreated on every render.

Defining CustomIcon inside NodeLocIcon means a new function reference is created on each render, which may cause unnecessary re-renders of the Icon component.

♻️ Proposed refactor - extract CustomIcon
+const CustomIcon = (props) => (
+  <svg
+    className='icon'
+    viewBox='0 0 102 64'
+    version='1.1'
+    xmlns='http://www.w3.org/2000/svg'
+    width='1em'
+    height='1em'
+    {...props}
+  >
+    <g id='nodeloc_icon' data-name='nodeloc_icon'>
+      <path d="M51.504 19.32..." fill="url(`#paint0_linear_1_6`)"/>
+      <defs>
+        <linearGradient id="paint0_linear_1_6" x1="-6" y1="35" x2="104" y2="35" gradientUnits="userSpaceOnUse">
+          <stop stop-color="#009966"/>
+          <stop offset="0.790909" stop-color="#FF9933"/>
+        </linearGradient>
+      </defs>
+    </g>
+  </svg>
+);

 const NodeLocIcon = (props) => {
-  function CustomIcon() {
-    return (
-      <svg ... {...props}>
-        ...
-      </svg>
-    );
-  }
-
-  return <Icon svg={<CustomIcon />} {...props} />;
+  return <Icon svg={<CustomIcon />} {...props} />;
 };
web/src/components/auth/RegisterForm.jsx (2)

311-318: Missing user agreement/privacy policy consent check.

Unlike LoginForm.jsx (lines 363-368), this handler doesn't verify agreedToTerms before initiating OAuth. Users could bypass consent by using NodeLoc OAuth registration. For consistency with LoginForm behavior, consider adding the consent check.

Proposed fix
 const handleNodeLocClick = () => {
+  if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
+    showInfo(t('请先阅读并同意用户协议和隐私政策'));
+    return;
+  }
   setNodelocLoading(true);
   try {
     onNodeLocOAuthClicked(status.nodeloc_client_id, { shouldLogout: true });
   } finally {
     setTimeout(() => setNodelocLoading(false), 3000);
   }
 };

467-474: Inline color style won't affect gradient-filled SVG.

The NodeLocIcon SVG uses a gradient fill (fill="url(#paint0_linear_1_6)"), so the inline color: '#2563eb' style will have no visual effect. Either remove the unused style prop or update the icon component to support color customization via currentColor.

web/src/components/auth/LoginForm.jsx (1)

574-581: Same note: inline color style won't affect gradient-filled SVG.

As noted for RegisterForm.jsx, the NodeLocIcon uses gradient fill, so this color style has no effect.

web/src/components/settings/SystemSetting.jsx (3)

1581-1630: Missing redirect URL Banner for NodeLoc OAuth configuration.

Other OAuth configuration sections (GitHub at lines 1467-1470, LinuxDO at lines 1544-1547) include a Banner showing the callback/redirect URL. This helps users configure the OAuth app correctly. Consider adding a similar banner here.

Proposed fix
 <Card>
   <Form.Section text={t('配置 NodeLoc OAuth App')}>
     <Text>
       {t(
         '用以支持通过 NodeLoc 进行登录注册,'
       )}
       <a
         href='https://docs.nodeloc.com/api-reference/introduction'
         target='_blank'
         rel='noreferrer'
         style={{
           display: 'inline-block',
           marginLeft: 4,
           marginRight: 4,
         }}
       >
         {t('点击查看详细文档')}
       </a>
     </Text>
+    <Banner
+      type='info'
+      description={`${t('回调 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/nodeloc`}
+      style={{ marginBottom: 20, marginTop: 16 }}
+    />
     <Row

125-125: Unused state variable nodeLocOAuthEnabled.

This state is set in handleCheckboxChange (line 722) but never read. The same applies to linuxDOOAuthEnabled (line 124). If these aren't needed for future functionality, consider removing them.


1619-1623: Missing i18n translation wrappers.

The label and placeholder for NodeLocMinimumTrustLevel are not wrapped in t() for internationalization. Note: the same issue exists in the LinuxDO section (lines 1569-1572).

Proposed fix
 <Form.Input
   field='NodeLocMinimumTrustLevel'
-  label='NodeLoc Minimum Trust Level'
-  placeholder='允许注册的最低信任等级'
+  label={t('NodeLoc Minimum Trust Level')}
+  placeholder={t('允许注册的最低信任等级')}
 />

Comment on lines +15 to +18
访问 NodeLoc 论坛创建 OAuth 应用:
```
https://www.nodeloc.com/oauth-provider/applications
```
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add code-fence languages and avoid bare URLs.
This aligns with markdown linting and improves readability.

✅ Suggested doc fixes
-```
+```text
 https://www.nodeloc.com/oauth-provider/applications

@@

  • https://www.nodeloc.com/oauth-provider/authorize?
      client_id=YOUR_CLIENT_ID&
      redirect_uri=YOUR_REDIRECT_URI&
      response_type=code&
      scope=openid%20profile&
      state=RANDOM_STATE
    

@@
-- NodeLoc OAuth 文档: https://docs.nodeloc.com/api-reference/introduction
+- NodeLoc OAuth 文档: https://docs.nodeloc.com/api-reference/introduction

</details>


Also applies to: 60-67, 108-110

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

16-16: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @docs/nodeloc-oauth.md around lines 15 - 18, Update the markdown snippets in
docs/nodeloc-oauth.md to use fenced code blocks with a language specifier (e.g.,

angle-bracketed links (e.g., <https://...>) so they are not treated as plain
bare URLs; specifically change the inline blocks containing
"https://www.nodeloc.com/oauth-provider/applications" and the authorize URL
block (client_id/redirect_uri/response_type...) to ```text fences, and update
occurrences like "https://docs.nodeloc.com/api-reference/introduction" to use
<...> angle-bracket notation; apply the same changes to the other occurrences
noted (lines around 60-67 and 108-110).

nodeloc and others added 2 commits January 27, 2026 14:23
- Add safe type assertions to prevent panics (session ID, oauth_state, affCode)
- Add HTTP status code validation for token and userinfo endpoints
- Fix username race condition using timestamp+NodeLocID combination
- Add docstrings for Go functions (NodeLocUser, NodeLocBind, getNodeLocUserInfoByCode, NodeLocOAuth)
- Add NodeLoc config defaults in InitOptionMap (ClientId, ClientSecret, MinimumTrustLevel)
- Fix OAuth2Callback error redirect to /login (was /console/personal)
- Add status.nodeloc_oauth to LoginForm and RegisterForm display conditions
- Fix SVG attributes in NodeLocIcon (stop-color -> stopColor)
- Remove unused antd dependency from package.json

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@web/src/components/auth/OAuth2Callback.jsx`:
- Around line 82-90: The useEffect currently only validates the authorization
code; also validate the OAuth2 state parameter before contacting the backend: in
OAuth2Callback.jsx inside the useEffect (where code and state are read), add a
guard for a missing or empty state (similar to the existing !code block) that
calls showError(...) with a localized message, navigates back to '/login' via
navigate('/login'), and returns early to avoid sending an incomplete payload to
the backend; ensure this check runs before any backend exchange call.
- Around line 68-74: The retry currently in sendCode(code, state, retry) retries
for all errors; change it to only retry when the error is a network error (i.e.,
no HTTP response was received) by checking that error.response is undefined/null
before performing the exponential/backoff retry; keep the existing MAX_RETRIES
logic, wait/backoff behavior ((retry + 1) * 2000) and recursive call to
sendCode(code, state, retry + 1), but immediately rethrow or propagate the error
when error.response exists (HTTP 4xx/5xx) so those failures do not trigger
retries.
🧹 Nitpick comments (1)
controller/nodeloc.go (1)

100-104: Consider adding timeout context for external HTTP calls.

The HTTP client has a 5-second timeout which is good, but for production resilience, consider using context.WithTimeout derived from the request context (c.Request.Context()) to properly cancel requests if the client disconnects.

Comment on lines 68 to 74
} catch (error) {
// 网络错误才重试
if (retry < MAX_RETRIES) {
// 递增的退避等待
await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 2000));
return sendCode(code, state, retry + 1);
}
Copy link
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:

cat -n web/src/components/auth/OAuth2Callback.jsx | head -100

Repository: QuantumNous/new-api

Length of output: 3490


🏁 Script executed:

# Check the broader context around the sendCode function
sed -n '40,85p' web/src/components/auth/OAuth2Callback.jsx

Repository: QuantumNous/new-api

Length of output: 1280


🌐 Web query:

Axios error detection network error vs HTTP response error handling

💡 Result:

In Axios, you generally distinguish HTTP response errors (server replied with a status code) from network / no-response errors (browser/node never got an HTTP response) by checking error.response vs error.request. Axios documents this exact branching pattern. [1]

import axios from "axios";

try {
  const res = await axios.get("/api");
  // success (2xx by default)
} catch (error) {
  if (!axios.isAxiosError(error)) throw error;

  if (error.response) {
    // HTTP response received (non-2xx by default)
    const { status, data, headers } = error.response;
    // handle status-based logic here (401/403/429/500/etc)
  } else if (error.request) {
    // Request was sent but NO response was received
    // Often shows: message "Network Error", code "ERR_NETWORK"
    // Causes include connectivity/DNS, CORS blocking, blocked/mixed-content, etc. [1][2]
  } else {
    // Something happened setting up the request
    // error.message / error.config
  }
}

Key points:

  • Non-2xx is an error by default and is handled via error.response.status, error.response.data, etc. [1]
  • “Network Error” / code: "ERR_NETWORK" typically means no HTTP response was available to Axios (so error.response is missing). [1][2]
  • If you want to handle certain “error” status codes (or all status codes) without throwing, use validateStatus. [1]
const res = await axios.get("/api", {
  validateStatus: (status) => status < 500, // example
});

If you’re seeing ERR_NETWORK but expect an HTTP status (e.g., 429) in the browser, it usually indicates the response is being prevented from reaching JS (commonly CORS / browser-layer blocking), so Axios can’t populate error.response. This kind of symptom is commonly reported. [3]

Sources: Axios error handling docs [1], explanation of Axios “Network Error” meaning [2], community report of 429 surfacing as ERR_NETWORK [3].


Limit retries to actual network errors

The current retry logic repeats all errors, including 4xx/5xx HTTP responses from the OAuth endpoint, which causes unnecessary delays for invalid codes or authorization failures. The comment "网络错误才重试" is not enforced. Gate retries on network-only errors (check for error.response absence, which indicates no HTTP response was received).

Proposed fix
     } catch (error) {
-      // 网络错误才重试
-      if (retry < MAX_RETRIES) {
+      // 网络错误才重试(无 HTTP 响应)
+      const isNetworkError =
+        error?.response == null;
+      if (isNetworkError && retry < MAX_RETRIES) {
         // 递增的退避等待
         await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 2000));
         return sendCode(code, state, retry + 1);
       }
🤖 Prompt for AI Agents
In `@web/src/components/auth/OAuth2Callback.jsx` around lines 68 - 74, The retry
currently in sendCode(code, state, retry) retries for all errors; change it to
only retry when the error is a network error (i.e., no HTTP response was
received) by checking that error.response is undefined/null before performing
the exponential/backoff retry; keep the existing MAX_RETRIES logic, wait/backoff
behavior ((retry + 1) * 2000) and recursive call to sendCode(code, state, retry
+ 1), but immediately rethrow or propagate the error when error.response exists
(HTTP 4xx/5xx) so those failures do not trigger retries.

Comment on lines 82 to 90
useEffect(() => {
const code = searchParams.get('code');
const state = searchParams.get('state');

// 参数缺失直接返回
// 参数缺失直接返回登录页
if (!code) {
showError(t('未获取到授权码'));
navigate('/console/personal');
navigate('/login');
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate state before calling the backend
state can be missing; guard it to avoid sending an incomplete OAuth payload and to show a clearer error early.

✅ Proposed fix
-    if (!code) {
+    if (!code || !state) {
       showError(t('未获取到授权码'));
       navigate('/login');
       return;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const code = searchParams.get('code');
const state = searchParams.get('state');
// 参数缺失直接返回
// 参数缺失直接返回登录页
if (!code) {
showError(t('未获取到授权码'));
navigate('/console/personal');
navigate('/login');
return;
useEffect(() => {
const code = searchParams.get('code');
const state = searchParams.get('state');
// 参数缺失直接返回登录页
if (!code || !state) {
showError(t('未获取到授权码'));
navigate('/login');
return;
🤖 Prompt for AI Agents
In `@web/src/components/auth/OAuth2Callback.jsx` around lines 82 - 90, The
useEffect currently only validates the authorization code; also validate the
OAuth2 state parameter before contacting the backend: in OAuth2Callback.jsx
inside the useEffect (where code and state are read), add a guard for a missing
or empty state (similar to the existing !code block) that calls showError(...)
with a localized message, navigates back to '/login' via navigate('/login'), and
returns early to avoid sending an incomplete payload to the backend; ensure this
check runs before any backend exchange call.

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.

1 participant