Skip to content

Conversation

nodeloc
Copy link

@nodeloc nodeloc commented Sep 11, 2025

Add Nodeloc Oauth2 login.

Summary by CodeRabbit

  • New Features

    • NodeLoc OAuth: login, registration, and account binding with NodeLoc ID stored on profiles; NodeLoc login buttons and icon.
  • Changes

    • Settings UI to enable/configure NodeLoc OAuth and display callback URL.
    • Status/API exposes NodeLoc-related fields; new OAuth callback route added.
    • Telegram OAuth option removed from auth screens.
    • Docker image/name updated.
  • Chores

    • CI workflow added to build/publish multi-arch Docker images.
  • Documentation

    • API docs and Docker deployment guide updated for NodeLoc OAuth.

Copy link
Contributor

coderabbitai bot commented Sep 11, 2025

Walkthrough

Adds end-to-end NodeLoc OAuth: new config constants and status fields, user model NodeLocId and lookup, controller handlers for OAuth callback and binding, option validation/mapping, router endpoint, frontend route/UI/icon/helper, settings UI, docs, Docker CI workflow, and image reference update.

Changes

Cohort / File(s) Summary
Backend config constants
common/constants.go
Adds NodeLoc config vars: NodeLocOAuthEnabled, NodeLocClientId, NodeLocClientSecret, NodeLocMinimumTrustLevel.
Status exposure
controller/misc.go
Exposes nodeloc_oauth, nodeloc_client_id, nodeloc_minimum_trust_level in GetStatus JSON.
NodeLoc OAuth controllers
controller/nodeloc.go
New NodeLocUser type; handlers NodeLocOAuth, NodeLocBind; helper getNodeLocUserInfoByCode to exchange code/token and fetch userinfo; login/registration/binding flows integrated with session and models.
Options validation & mapping
controller/option.go, model/option.go
Validates enabling NodeLoc requires client id/secret; maps NodeLoc option keys into common variables and update paths.
User model binding
model/user.go
Adds User.NodeLocId (column nodeloc_id), IsNodeLocIdAlreadyTaken, and (user *User) FillUserByNodeLocId().
Routing & API endpoint
router/api-router.go, docs/api/web_api.md
Registers GET /oauth/nodeloc routed to NodeLocOAuth; documents GET /api/oauth/nodeloc.
Frontend SPA route & helpers
web/src/App.jsx, web/src/helpers/api.js
Adds SPA route /oauth/nodeloc; adds onNodeLocAuthClicked(nodeloc_client_id) to construct NodeLoc authorize URL and open it with state and server-aware redirect_uri.
Auth UI (Login & Register)
web/src/components/auth/LoginForm.jsx, web/src/components/auth/RegisterForm.jsx
Adds NodeLoc OAuth button(s) with NodeLocIcon, loading state and click handlers; replaces Telegram option; gated by status.nodeloc_oauth.
Account binding UI
web/src/components/settings/personal/cards/AccountManagement.jsx
Adds NodeLoc binding card showing user?.nodeloc_id and bind button (calls onNodeLocAuthClicked); disabled if already bound or feature disabled.
Settings UI
web/src/components/settings/SystemSetting.jsx
Adds NodeLoc OAuth enable checkbox, Client ID/Client Secret inputs, save logic, and callback URL display.
UI assets
web/src/components/common/logo/NodeLocIcon.jsx
New NodeLocIcon React component (inline SVG wrapped in Icon).
Docker docs & CI
DOCKER_README.md, .github/workflows/docker-publish.yml, docker-compose.yml
Adds Docker deployment README (Chinese); adds GitHub Actions workflow to build/push nodeloc/new-api; updates compose image to nodeloc/new-api:latest.
Self response
controller/user.go
Adds nodeloc_id to GetSelf JSON response sourced from user.NodeLocId.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser
  participant SPA as Web SPA
  participant API as API Router
  participant Ctrl as NodeLoc Controller
  participant OAuth as NodeLoc OAuth Server
  participant Model as User Model
  participant Session as Session/Auth

  Browser->>SPA: Click "Continue with NodeLoc"
  SPA->>SPA: getOAuthState()
  SPA->>Browser: Open NodeLoc authorize URL (client_id,state,redirect_uri)
  Browser->>OAuth: User authorizes → returns code
  Browser->>API: GET /oauth/nodeloc?code&state
  API->>Ctrl: NodeLocOAuth handler
  Ctrl->>Session: Validate state/session
  Ctrl->>OAuth: POST /token (code + client creds)
  OAuth-->>Ctrl: access_token
  Ctrl->>OAuth: GET /userinfo (Bearer token)
  OAuth-->>Ctrl: NodeLocUser{sub, username, email, groups}
  alt Binding flow (session user present)
    Ctrl->>Model: IsNodeLocIdAlreadyTaken?
    Ctrl->>Model: Update current user's NodeLocId
    Ctrl-->>Browser: Binding result
  else Login/Registration flow
    Ctrl->>Model: FillUserByNodeLocId / find user
    alt Found
      Ctrl->>Session: setupLogin(user)
      Ctrl-->>Browser: Login success
    else Not found
      alt Registration enabled
        Ctrl->>Model: Create user & persist
        Ctrl->>Session: setupLogin(new user)
        Ctrl-->>Browser: Registration + login success
      else
        Ctrl-->>Browser: Registration disabled message
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: auto分组 #1239 — Also modifies GetStatus in controller/misc.go to add new keys to the status response; touches the same function and may conflict.

Poem

I hop to the OAuth gate with glee,
Codes and curls of token tea;
I bind an id, fetch name and band,
NodeLoc springs from paw and hand.
Carrots, routes, and icons — whee! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 "add nodeloc oauth2 login" succinctly and accurately summarizes the primary change — adding NodeLoc OAuth2 login support — and aligns with the PR objectives and the modified files (controllers, models, UI, settings, routes, and docs). It is short, focused, and not misleading.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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: 8

🧹 Nitpick comments (15)
model/user.go (2)

911-915: Tighten “exists” check and avoid full row load

Use a COUNT/SELECT id-only to reduce I/O and keep intent explicit.

-func IsNodeLocIdAlreadyTaken(nodeLocId string) bool {
-	var user User
-	err := DB.Unscoped().Where("nodeloc_id = ?", nodeLocId).First(&user).Error
-	return !errors.Is(err, gorm.ErrRecordNotFound)
-}
+func IsNodeLocIdAlreadyTaken(nodeLocId string) bool {
+	var cnt int64
+	_ = DB.Unscoped().Model(&User{}).Where("nodeloc_id = ?", nodeLocId).Count(&cnt).Error
+	return cnt > 0
+}

917-923: Consistency and UX: align not-found error with other Fill helpers*

Other Fill* helpers sometimes return friendly messages (e.g., Telegram). Consider returning a consistent, user-friendly error when not found.

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

33-34: Prop forwarding: forward to Icon, not the inner SVG

Consumers expect size/className/etc. to affect the Icon wrapper. Forward props to Icon; avoid double-spreading.

-        {...props}
+        /* props forwarded on <Icon /> */
-  return <Icon svg={<CustomIcon />} />;
+  return <Icon svg={<CustomIcon />} {...props} />;

Also applies to: 46-47

controller/option.go (1)

89-96: Validate both Client ID and Secret before enabling NLAuthEnabled

Message mentions both, but only ClientId is checked.

-	case "NLAuthEnabled":
-		if option.Value == "true" && common.NodeLocClientId == "" {
+	case "NLAuthEnabled":
+		if option.Value == "true" && (common.NodeLocClientId == "" || common.NodeLocClientSecret == "") {
 			c.JSON(http.StatusOK, gin.H{
 				"success": false,
 				"message": "无法启用 NodeLoc OAuth,请先填入 NodeLoc Client Id 以及 NodeLoc Client Secret!",
 			})
 			return
 		}
web/src/components/settings/personal/cards/AccountManagement.jsx (1)

364-404: Use NodeLoc icon and guard missing client_id

  • Use the dedicated NodeLocIcon instead of SiLinux.
  • Disable the bind button when status.nodeloc_client_id is falsy to prevent opening an invalid OAuth URL.

Apply within this block:

-                      <SiLinux
-                        size={20}
-                        className='text-slate-600 dark:text-slate-300'
-                      />
+                      <span className='text-slate-600 dark:text-slate-300'>
+                        {/* Keep size parity with other icons */}
+                        <NodeLocIcon style={{ width: 20, height: 20 }} />
+                      </span>
...
-                      disabled={
+                      disabled={
                         (userState.user && userState.user.nodeloc_id !== '') ||
-                        !status.nodeloc_oauth
+                        !status.nodeloc_oauth ||
+                        !status.nodeloc_client_id
                       }

Add import near the top of the file:

import NodeLocIcon from '../../../common/logo/NodeLocIcon';
web/src/components/auth/RegisterForm.jsx (2)

257-264: Guard missing NodeLoc Client ID before redirect

Avoid opening a malformed OAuth URL when status.nodeloc_client_id is empty.

 const handleNodeLocClick = () => {
   setNodelocLoading(true);
   try {
+    if (!status.nodeloc_client_id) {
+      showError(t('未配置 NodeLoc Client ID'));
+      return;
+    }
     onNodeLocAuthClicked(status.nodeloc_client_id);
   } finally {
     setTimeout(() => setNodelocLoading(false), 3000);
   }
 };

393-412: Disable NodeLoc button if client_id is missing; keep brand color consistent

  • Disable when !status.nodeloc_client_id.
  • Consider using the same icon color as Login for consistency (pick one).
-                {status.nodeloc_oauth && (
+                {status.nodeloc_oauth && (
                   <Button
                     theme='outline'
                     className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
                     type='tertiary'
                     icon={
                       <NodeLocIcon
                         style={{
-                          color: '#4A90E2',
+                          color: '#4A90E2', /* align with Login or update both */
                           width: '20px',
                           height: '20px',
                         }}
                       />
                     }
                     onClick={handleNodeLocClick}
                     loading={nodelocLoading}
+                    disabled={!status.nodeloc_client_id}
                   >
                     <span className='ml-3'>{t('使用 NodeLoc 继续')}</span>
                   </Button>
                 )}

Please also align the NodeLoc icon color in Login for visual consistency.

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

265-273: Guard missing NodeLoc Client ID before redirect

Prevent malformed OAuth attempts when nodeloc_client_id is unset.

 const handleNodeLocAuthClick = () => {
   setNodelocLoading(true);
   try {
+    if (!status.nodeloc_client_id) {
+      showError(t('未配置 NodeLoc Client ID'));
+      return;
+    }
     onNodeLocAuthClicked(status.nodeloc_client_id);
   } finally {
     // 由于重定向,这里不会执行到,但为了完整性添加
     setTimeout(() => setNodelocLoading(false), 3000);
   }
 };

390-409: Disable button if client_id missing; unify NodeLoc icon color

  • Add disabled={!status.nodeloc_client_id}.
  • Consider unifying icon color with Register.
                 {status.nodeloc_oauth && (
                   <Button
                     theme='outline'
                     className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
                     type='tertiary'
                     icon={
                       <NodeLocIcon
                         style={{
-                          color: '#E95420',
+                          color: '#4A90E2', /* or update Register to #E95420 */
                           width: '20px',
                           height: '20px',
                         }}
                       />
                     }
                     onClick={handleNodeLocAuthClick}
                     loading={nodelocLoading}
+                    disabled={!status.nodeloc_client_id}
                   >
                     <span className='ml-3'>{t('使用 NodeLoc 继续')}</span>
                   </Button>
                 )}
web/src/components/settings/SystemSetting.jsx (3)

103-104: Remove unused local state

nodeLocOAuthEnabled is set but never used; drop it to reduce noise.

-  const [nodeLocOAuthEnabled, setNodeLocOAuthEnabled] = useState(false);

Also remove its setter usage below (see next suggestion).


529-531: Remove unused setter

No consumer reads nodeLocOAuthEnabled; this can go.

-    if (optionKey === 'NodeLocOAuthEnabled') {
-      setNodeLocOAuthEnabled(value);
-    }

497-516: Consider supporting NodeLoc minimum trust level here

If NodeLocMinimumTrustLevel is intended (see backend constants), include it in this submit handler, similar to LinuxDO’s trust level handling.

Would you like a patch adding the input and update logic for NodeLocMinimumTrustLevel?

controller/nodeloc.go (3)

221-225: Make username generation deterministic and include email when available

GetMaxUserId()+1 risks collisions under concurrency. Prefer a deterministic, low-collision username and store email if provided.

-      user.Username = "nodeloc_" + strconv.Itoa(model.GetMaxUserId()+1)
-      user.DisplayName = nodeLocUser.Username
+      // Prefer provider-subject based username; ensure DB uniqueness at model level.
+      user.Username = "nodeloc_" + nodeLocUser.Sub
+      user.DisplayName = nodeLocUser.Username
+      if nodeLocUser.Email != "" {
+        user.Email = nodeLocUser.Email
+      }

183-189: Enforce minimum trust level (config is present but unused)

common.NodeLocMinimumTrustLevel exists, and NodeLocUser.Groups is returned, but no enforcement occurs. If applicable, reject accounts below the configured trust level before creating/logging in the user.

Would you like me to draft a guard that maps NodeLoc groups to a numeric trust level and enforces NodeLocMinimumTrustLevel?


72-76: Response message clarity

Responding with "message": "bind" is terse for clients/UI. Consider a clearer message like "NodeLoc 账户绑定成功" for consistency with other handlers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 041782c and bcca071.

📒 Files selected for processing (14)
  • common/constants.go (2 hunks)
  • controller/misc.go (1 hunks)
  • controller/nodeloc.go (1 hunks)
  • controller/option.go (1 hunks)
  • model/option.go (2 hunks)
  • model/user.go (2 hunks)
  • router/api-router.go (1 hunks)
  • web/src/App.jsx (1 hunks)
  • web/src/components/auth/LoginForm.jsx (7 hunks)
  • web/src/components/auth/RegisterForm.jsx (6 hunks)
  • web/src/components/common/logo/NodeLocIcon.jsx (1 hunks)
  • web/src/components/settings/SystemSetting.jsx (7 hunks)
  • web/src/components/settings/personal/cards/AccountManagement.jsx (2 hunks)
  • web/src/helpers/api.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (10)
router/api-router.go (1)
controller/nodeloc.go (1)
  • NodeLocOAuth (155-257)
web/src/App.jsx (2)
web/src/components/common/ui/Loading.jsx (1)
  • Loading (23-29)
web/src/components/auth/OAuth2Callback.jsx (1)
  • OAuth2Callback (33-93)
controller/misc.go (1)
common/constants.go (3)
  • NodeLocOAuthEnabled (47-47)
  • NodeLocClientId (89-89)
  • NodeLocMinimumTrustLevel (91-91)
controller/option.go (1)
common/constants.go (1)
  • NodeLocClientId (89-89)
web/src/components/settings/personal/cards/AccountManagement.jsx (2)
web/src/components/auth/LoginForm.jsx (2)
  • userState (62-62)
  • status (89-92)
web/src/helpers/api.js (1)
  • onNodeLocAuthClicked (266-272)
web/src/components/auth/RegisterForm.jsx (3)
web/src/components/auth/LoginForm.jsx (1)
  • status (89-92)
web/src/helpers/api.js (1)
  • onNodeLocAuthClicked (266-272)
web/src/components/common/logo/NodeLocIcon.jsx (1)
  • NodeLocIcon (23-47)
model/option.go (1)
common/constants.go (2)
  • OptionMap (36-36)
  • NodeLocOAuthEnabled (47-47)
model/user.go (1)
model/main.go (1)
  • DB (63-63)
controller/nodeloc.go (3)
common/constants.go (6)
  • NodeLocOAuthEnabled (47-47)
  • NodeLocClientId (89-89)
  • NodeLocClientSecret (90-90)
  • RegisterEnabled (51-51)
  • RoleCommonUser (139-139)
  • UserStatusEnabled (179-179)
common/gin.go (1)
  • ApiError (95-100)
model/user.go (4)
  • User (19-49)
  • IsNodeLocIdAlreadyTaken (911-915)
  • GetMaxUserId (181-185)
  • GetUserIdByAffCode (302-309)
web/src/components/auth/LoginForm.jsx (3)
web/src/components/auth/RegisterForm.jsx (1)
  • nodelocLoading (80-80)
web/src/helpers/api.js (1)
  • onNodeLocAuthClicked (266-272)
web/src/components/common/logo/NodeLocIcon.jsx (1)
  • NodeLocIcon (23-47)
🔇 Additional comments (15)
model/user.go (1)

17-19: Verified — setupLogin does not return NodeLocId

setupLogin (controller/user.go, ~line 96) builds cleanUser with only Id, Username, DisplayName, Role, Status, Group — NodeLocId (nodeloc_id) is not included.

router/api-router.go (1)

33-33: Route addition LGTM

Endpoint and middleware align with existing OAuth routes.

web/src/App.jsx (1)

209-216: Callback route LGTM

Matches existing OAuth callback pattern with Suspense + Loading.

controller/misc.go (1)

54-56: Status fields LGTM

Names align with frontend usage; exposes only client_id (not secret).

common/constants.go (2)

47-47: LGTM: feature flag added

NodeLocOAuthEnabled toggle fits existing pattern.


89-92: MinimumTrustLevel is unused (dead config)

NodeLocMinimumTrustLevel is defined but never enforced in controller/nodeloc.go. Either wire it into the OAuth flow (e.g., reject low-trust accounts) or drop it to avoid confusion.

web/src/components/settings/personal/cards/AccountManagement.jsx (1)

47-47: LGTM: helper import

Importing onNodeLocAuthClicked is consistent with other providers.

web/src/components/auth/RegisterForm.jsx (4)

46-52: LGTM: NodeLoc wiring

Imports align with existing OAuth options and icon usage.


80-80: LGTM: loading state

nodelocLoading mirrors other provider states.


558-559: LGTM: gating updated

Including status.nodeloc_oauth in the “other options” condition is correct.


654-655: LGTM: primary/alt flow toggle

The added check ensures the email form shows when no OAuth is available.

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

46-52: LGTM: NodeLoc imports

Helper and icon imported consistently with other providers.


72-72: LGTM: loading state

Mirrors other provider loading flags.


647-648: LGTM: flow toggle updated

Including status.nodeloc_oauth in the check is correct.

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

89-92: LGTM: NodeLoc inputs added

State keys follow existing naming conventions.

Comment on lines +55 to +60
session := sessions.Default(c)
id := session.Get("id")
user.Id = id.(int)

err = user.FillUserById()
if err != nil {
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

Harden session ID extraction to avoid panic

id.(int) will panic if session is empty or stored type differs. Fail gracefully.

- id := session.Get("id")
- user.Id = id.(int)
+ idVal := session.Get("id")
+ userID, ok := idVal.(int)
+ if !ok || userID == 0 {
+   c.JSON(http.StatusUnauthorized, gin.H{
+     "success": false,
+     "message": "未登录或会话无效",
+   })
+   return
+ }
+ user.Id = userID
📝 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
session := sessions.Default(c)
id := session.Get("id")
user.Id = id.(int)
err = user.FillUserById()
if err != nil {
session := sessions.Default(c)
idVal := session.Get("id")
userID, ok := idVal.(int)
if !ok || userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录或会话无效",
})
return
}
user.Id = userID
err = user.FillUserById()
if err != nil {
🤖 Prompt for AI Agents
In controller/nodeloc.go around lines 55 to 60, the code uses a direct type
assertion id.(int) which will panic if the session value is nil or of a
different type; change this to safely validate and convert the session value
before assigning to user.Id: check session.Get("id") for nil, perform a type
switch or sequential type checks (handle int, int32/int64, float32/float64, and
string-by-parsing) to extract a stable int value, handle the failure case by
returning an error or redirecting instead of panicking, and only then assign to
user.Id and call FillUserById.

Comment on lines 109 to 126
client := http.Client{Timeout: 5 * time.Second}
res, err := client.Do(req)
if err != nil {
return nil, errors.New("failed to connect to NodeLoc server")
}
defer res.Body.Close()

var tokenRes struct {
AccessToken string `json:"access_token"`
Message string `json:"message"`
}
if err := json.NewDecoder(res.Body).Decode(&tokenRes); err != nil {
return nil, err
}

if tokenRes.AccessToken == "" {
return nil, fmt.Errorf("failed to get access token: %s", tokenRes.Message)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Check HTTP status codes and surface provider errors

You decode response bodies without checking StatusCode. Surface non-2xx errors for both token and userinfo endpoints.

   res, err := client.Do(req)
   if err != nil {
     return nil, errors.New("failed to connect to NodeLoc server")
   }
   defer res.Body.Close()
+  if res.StatusCode < 200 || res.StatusCode >= 300 {
+    var body struct{ Message string `json:"message"` }
+    _ = json.NewDecoder(res.Body).Decode(&body)
+    return nil, fmt.Errorf("token endpoint error: %s (HTTP %d)", body.Message, res.StatusCode)
+  }
...
   res2, err := client.Do(req)
   if err != nil {
     return nil, errors.New("failed to get user info from NodeLoc")
   }
   defer res2.Body.Close()
+  if res2.StatusCode < 200 || res2.StatusCode >= 300 {
+    var body struct{ Message string `json:"message"` }
+    _ = json.NewDecoder(res2.Body).Decode(&body)
+    return nil, fmt.Errorf("userinfo endpoint error: %s (HTTP %d)", body.Message, res2.StatusCode)
+  }

Also applies to: 137-146

🤖 Prompt for AI Agents
In controller/nodeloc.go around lines 109-126 (and similarly 137-146), the code
decodes the response body without checking HTTP StatusCode; update both token
and userinfo response handling to first check res.StatusCode and treat non-2xx
as errors: read the full response body (or decode into an error struct), include
the HTTP status code and any provider error/message in the returned error, and
only decode the success payload when status is 2xx; ensure bodies are still
closed and returned errors include both status and provider message for easier
diagnosis.

model/option.go Outdated
Comment on lines 40 to 41
common.OptionMap["NLAuthEnabled"] = strconv.FormatBool(common.NodeLocOAuthEnabled)
common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled)
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

Enable flag mismatch: NLAuthEnabled never toggles NodeLocOAuthEnabled

Init publishes NLAuthEnabled, but updateOptionMap only handles NodeLocOAuthEnabled. Enabling via UI won’t actually set the runtime flag.

Minimal fix: map NLAuthEnabled to the runtime flag.

-	common.OptionMap["NLAuthEnabled"] = strconv.FormatBool(common.NodeLocOAuthEnabled)
+	common.OptionMap["NLAuthEnabled"] = strconv.FormatBool(common.NodeLocOAuthEnabled)
 	if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" || key == "DefaultUseAutoGroup" {
 		boolValue := value == "true"
 		switch key {
+		case "NLAuthEnabled":
+			common.NodeLocOAuthEnabled = boolValue
 		case "NodeLocOAuthEnabled":
 			common.NodeLocOAuthEnabled = boolValue

Additionally, NodeLoc credentials are not synchronized from OptionMap to runtime. Add cases to updateOptionMap:

// Add alongside other credential cases
case "NodeLocClientId":
	common.NodeLocClientId = value
case "NodeLocClientSecret":
	common.NodeLocClientSecret = value
case "NodeLocMinimumTrustLevel":
	common.NodeLocMinimumTrustLevel, _ = strconv.Atoi(value)

Optionally expose defaults in InitOptionMap (mirroring GitHub/LinuxDO) so they appear in GetOptions:

common.OptionMap["NodeLocClientId"] = ""
common.OptionMap["NodeLocClientSecret"] = ""
common.OptionMap["NodeLocMinimumTrustLevel"] = strconv.Itoa(common.NodeLocMinimumTrustLevel)

Also applies to: 220-222

🤖 Prompt for AI Agents
In model/option.go around lines 40-41 (and similarly 220-222), the published
option key "NLAuthEnabled" is not mapped to the runtime flag
common.NodeLocOAuthEnabled and NodeLoc credential/options are not synchronized
back into runtime; update InitOptionMap to set OptionMap["NLAuthEnabled"] =
strconv.FormatBool(common.NodeLocOAuthEnabled] (instead of/alongside current
key) and add OptionMap entries for "NodeLocClientId", "NodeLocClientSecret", and
"NodeLocMinimumTrustLevel" (stringified default) so they appear in GetOptions;
then update updateOptionMap to handle keys "NLAuthEnabled" by setting
common.NodeLocOAuthEnabled from the parsed bool, and add cases
"NodeLocClientId", "NodeLocClientSecret", and "NodeLocMinimumTrustLevel" to set
common.NodeLocClientId, common.NodeLocClientSecret, and parse/assign
common.NodeLocMinimumTrustLevel respectively.

InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"`
NodeLocId string `json:"nodeloc_id" gorm:"column:nodeloc_id;index"`
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make NodeLocId unique to prevent multi-account binds

Index-only allows duplicates. Add a unique index to enforce 1:1 binding at DB level and avoid race conditions.

-	NodeLocId        string         `json:"nodeloc_id" gorm:"column:nodeloc_id;index"`
+	NodeLocId        string         `json:"nodeloc_id" gorm:"column:nodeloc_id;uniqueIndex"`
📝 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
NodeLocId string `json:"nodeloc_id" gorm:"column:nodeloc_id;index"`
NodeLocId string `json:"nodeloc_id" gorm:"column:nodeloc_id;uniqueIndex"`
🤖 Prompt for AI Agents
In model/user.go around line 45, the NodeLocId field currently has a non-unique
index which allows duplicate values; change its GORM tag to create a unique
index (e.g., use `uniqueIndex` or `unique` depending on your GORM version) so
the struct tag enforces a unique constraint at the DB level; update any existing
migrations/auto-migrate or add a new migration to alter the column to UNIQUE
(and handle existing duplicate rows before applying), then run migrations and
add/update tests to ensure 1:1 binding is enforced.

Comment on lines +38 to +40
<stop stop-color="#009966"/>
<stop offset="0.790909" stop-color="#FF9933"/>
</linearGradient>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix JSX SVG attributes

Use camelCase in React SVG: stopColor instead of stop-color. Current form triggers React warnings and may not apply gradient.

-        <stop stop-color="#009966"/>
-        <stop offset="0.790909" stop-color="#FF9933"/>
+        <stop stopColor="#009966"/>
+        <stop offset="0.790909" stopColor="#FF9933"/>
📝 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
<stop stop-color="#009966"/>
<stop offset="0.790909" stop-color="#FF9933"/>
</linearGradient>
<stop stopColor="#009966"/>
<stop offset="0.790909" stopColor="#FF9933"/>
</linearGradient>
🤖 Prompt for AI Agents
In web/src/components/common/logo/NodeLocIcon.jsx around lines 38 to 40, the SVG
uses hyphenated attributes (e.g., stop-color) which trigger React warnings and
may prevent gradients from applying; update these to React's camelCase SVG props
(change stop-color to stopColor, and verify any other hyphenated SVG attributes
in the file are converted to camelCase) so the gradient renders without
warnings.

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: 0

🧹 Nitpick comments (1)
docs/api/web_api.md (1)

198-198: Update the “更新日期”.

This edit changes public API docs; bump the date to reflect the latest modification.

- > **更新日期**:2025.07.17
+ > **更新日期**:2025.09.12
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcca071 and a77601f.

📒 Files selected for processing (2)
  • docs/api/web_api.md (1 hunks)
  • model/option.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • model/option.go
🔇 Additional comments (1)
docs/api/web_api.md (1)

47-47: NodeLoc OAuth endpoint doc looks good; consider documenting bind endpoint for parity.

LGTM on path/name consistency (lowercase path, proper display name). If a NodeLoc “bind” flow exists (controller/nodeloc.go), add a row to match WeChat/Telegram for completeness.

Proposed doc addition:

 | GET | /api/oauth/linuxdo | 公开 | LinuxDo OAuth 跳转 |
 | GET | /api/oauth/nodeloc | 公开 | NodeLoc OAuth 跳转 |
+| GET | /api/oauth/nodeloc/bind | 公开 | NodeLoc 账户绑定 |
 | GET | /api/oauth/wechat | 公开 | 微信扫码登录跳转 |

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: 4

🧹 Nitpick comments (8)
docker-compose.yml (1)

5-5: Avoid floating tag; make image tag overridable (or pin) for reproducible deploys.

Using latest can cause surprise upgrades. Make the tag configurable via env, or pin to a version/digest.

-    image: nodeloc/new-api:latest
+    image: nodeloc/new-api:${NEW_API_TAG:-latest}

Optionally pin:

image: nodeloc/new-api@sha256:<immutable_digest>
DOCKER_README.md (3)

32-37: Clarify callback URL format and HTTP/HTTPS requirements.

Explicitly note scheme must match the app registration and that local testing can use http://localhost:3000/api/oauth/nodeloc.

-3. 设置回调URL: `https://your-domain.com/api/oauth/nodeloc`
+3. 设置回调URL:生产环境使用 `https://your-domain.com/api/oauth/nodeloc`;
+   本地调试可使用 `http://localhost:3000/api/oauth/nodeloc`(需与应用配置一致)

17-17: Fix markdownlint MD034 (bare URLs).

Wrap bare URLs in angle brackets or convert to markdown links.

Example:

-3. 访问 http://localhost:3000
+3. 访问 <http://localhost:3000>

Apply similarly to lines with https://conn.nodeloc.cc/apps and the GitHub URLs.

Also applies to: 32-32, 45-46


40-41: Align documented image tags with CI tagging strategy.

Workflow generates semver tags from Git tags and “latest” on default branch; consider documenting branch tags (e.g., main) if they are pushed, or state they are internal only.

.github/workflows/docker-publish.yml (4)

3-9: Consider adding workflow_dispatch and concurrency to control runs.

This helps manual releases and cancels superseded builds on fast pushes.

Outside this hunk:

on:
  workflow_dispatch: {}
concurrency:
  group: docker-publish-${{ github.ref }}
  cancel-in-progress: true

36-47: Tag policy check: pushing branch tags (e.g., main) may be unintended.

If you only want semver and latest, drop ref-based tags or mark them disabled on push.

Possible adjustment:

         tags: |
-          type=ref,event=branch
-          type=ref,event=pr
+          # type=ref,event=branch
+          # type=ref,event=pr
           type=semver,pattern={{version}}
           type=semver,pattern={{major}}.{{minor}}
           type=raw,value=latest,enable={{is_default_branch}}

48-57: Supply-chain hardening: add provenance/SBOM; keep cache.

Attestations and SBOM improve traceability. No behavioral change.

       uses: docker/build-push-action@v5
       with:
         context: .
         platforms: linux/amd64,linux/arm64
         push: ${{ github.event_name != 'pull_request' }}
         tags: ${{ steps.meta.outputs.tags }}
         labels: ${{ steps.meta.outputs.labels }}
         cache-from: type=gha
         cache-to: type=gha,mode=max
+        provenance: true
+        sbom: true

29-35: Guard against missing Docker Hub secrets on forks.

Explicit check yields clearer failure messages.

       with:
         registry: ${{ env.REGISTRY }}
-        username: ${{ secrets.DOCKER_USERNAME }}
-        password: ${{ secrets.DOCKER_PASSWORD }}
+        username: ${{ secrets.DOCKER_USERNAME || '' }}
+        password: ${{ secrets.DOCKER_PASSWORD || '' }}

Alternatively, add a precheck step:

- name: Verify Docker Hub secrets present
  if: github.event_name != 'pull_request'
  run: |
    test -n "${{ secrets.DOCKER_USERNAME }}" && test -n "${{ secrets.DOCKER_PASSWORD }}" || {
      echo "Missing DOCKER_* secrets"; exit 1; }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a77601f and 32ac1fa.

📒 Files selected for processing (3)
  • .github/workflows/docker-publish.yml (1 hunks)
  • DOCKER_README.md (1 hunks)
  • docker-compose.yml (2 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
DOCKER_README.md

17-17: Bare URL used

(MD034, no-bare-urls)


32-32: Bare URL used

(MD034, no-bare-urls)


45-45: Bare URL used

(MD034, no-bare-urls)


46-46: Bare URL used

(MD034, no-bare-urls)

Comment on lines +25 to +27
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enable QEMU for multi-arch emulation.

Without QEMU, arm64 builds may fail on x86 runners.

     - name: Set up Docker Buildx
       uses: docker/setup-buildx-action@v3
+
+    - name: Set up QEMU
+      uses: docker/setup-qemu-action@v3
📝 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
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
🤖 Prompt for AI Agents
.github/workflows/docker-publish.yml around lines 25-27: the workflow currently
sets up Docker Buildx but does not register QEMU for multi-arch emulation, which
can cause arm64 builds to fail on x86 runners; add a step before the Buildx
setup that runs the official QEMU action (docker/setup-qemu-action@v2) to
register QEMU user-static binfmt handlers (optionally specifying platforms: all
or linux/amd64,linux/arm64) so emulation is enabled for cross-architecture
builds.

Comment on lines +9 to +10
curl -O https://raw.githubusercontent.com/nodeloc/new-api/main/docker-compose.yml
```
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Repository link likely incorrect.

The curl URL points to nodeloc/new-api, but this PR lives under QuantumNous/new-api. Use the current repository path or reference a release asset instead to avoid fetching the wrong compose.

Proposed fix:

-curl -O https://raw.githubusercontent.com/nodeloc/new-api/main/docker-compose.yml
+curl -O https://raw.githubusercontent.com/QuantumNous/new-api/main/docker-compose.yml

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "PWD: $(pwd)"
if command -v rg >/dev/null 2>&1; then
  SEARCH_TOOL="rg"
else
  SEARCH_TOOL=""
fi

echo "---- searching for literal 'nodeloc/new-api' ----"
if [ -n "$SEARCH_TOOL" ]; then
  rg -n --hidden -S 'nodeloc/new-api' || true
else
  grep -nR --exclude-dir=.git -e 'nodeloc/new-api' . || true
fi

echo "---- locating DOCKER_README.md files ----"
if command -v fd >/dev/null 2>&1; then
  fd -HI --hidden --type f 'DOCKER_README.md' || true
else
  find . -type f -iname 'DOCKER_README.md' -print || true
fi

echo "---- printing DOCKER_README.md contents (first 200 lines) if found ----"
while IFS= read -r file; do
  echo "---- FILE: $file ----"
  nl -ba "$file" | sed -n '1,200p' || true
done < <(find . -type f -iname 'DOCKER_README.md' -print)

echo "---- searching for any raw.githubusercontent.com occurrences ----"
if [ -n "$SEARCH_TOOL" ]; then
  rg -n --hidden -S 'raw.githubusercontent.com' || true
else
  grep -nR --exclude-dir=.git -e 'raw.githubusercontent.com' . || true
fi

Length of output: 621


Replace nodeloc/new-api references with QuantumNous/new-api or a tagged release asset

  • Instances to update: docker-compose.yml:5 (image: nodeloc/new-api:latest); DOCKER_README.md:9 (curl -O https://raw.githubusercontent.com/nodeloc/new-api/main/docker-compose.yml); DOCKER_README.md:45-46 (project & issues links); .github/workflows/docker-publish.yml:12 (IMAGE_NAME: nodeloc/new-api). Update each reference to the correct repository or a release asset.
🤖 Prompt for AI Agents
In DOCKER_README.md around lines 9 and 45-46, docker-compose.yml line 5, and
.github/workflows/docker-publish.yml line 12, replace all occurrences of the
repository identifier "nodeloc/new-api" with "QuantumNous/new-api" or point to a
specific tagged release asset (preferred: use a tag like
"QuantumNous/new-api:vX.Y.Z") — update the curl URL in DOCKER_README.md to fetch
from raw.githubusercontent.com/QuantumNous/new-api/... (or the specific release
asset URL), change the docker-compose image: value to QuantumNous/new-api[:tag],
update project and issues links to github.com/QuantumNous/new-api, and set
IMAGE_NAME in the workflow to QuantumNous/new-api (or include the tag) so all
references consistently point to the correct repo or release asset.

Comment on lines +20 to +28
### 其他配置
```yaml
environment:
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api
- REDIS_CONN_STRING=redis://redis
- TZ=Asia/Shanghai
- ERROR_LOG_ENABLED=true
- SESSION_SECRET=your_random_secret_key
```
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Document NodeLoc OAuth env vars alongside core settings.

Readers will miss how to wire OAuth unless the variables appear in the example block.

   - ERROR_LOG_ENABLED=true
   - SESSION_SECRET=your_random_secret_key
+  # NodeLoc OAuth2
+  - NODELOC_OAUTH_ENABLED=true
+  - NODELOC_CLIENT_ID=${NODELOC_CLIENT_ID}
+  - NODELOC_CLIENT_SECRET=${NODELOC_CLIENT_SECRET}
+  # optional
+  # - NODELOC_MINIMUM_TRUST_LEVEL=2
📝 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
### 其他配置
```yaml
environment:
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api
- REDIS_CONN_STRING=redis://redis
- TZ=Asia/Shanghai
- ERROR_LOG_ENABLED=true
- SESSION_SECRET=your_random_secret_key
```
### 其他配置
🤖 Prompt for AI Agents
In DOCKER_README.md around lines 20 to 28, the example environment YAML block is
missing NodeLoc OAuth variables; update that block to include the NodeLoc
OAuth-related environment variables (e.g., NODELOC_CLIENT_ID,
NODELOC_CLIENT_SECRET, NODELOC_REDIRECT_URI, NODELOC_AUTH_URL or
NODELOC_BASE_URL as your project expects) with placeholder values and brief
comments where appropriate so readers see how to wire OAuth alongside the
existing DB/redis/TZ/session settings.

Comment on lines 19 to 22
# NodeLoc OAuth2 配置
- NODELOC_OAUTH_ENABLED=true
- NODELOC_CLIENT_ID=your_client_id_here
- NODELOC_CLIENT_SECRET=your_client_secret_here
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Don’t hardcode OAuth credentials; load from .env or Docker secrets.

Placing client ID/secret in compose risks leakage via VCS/logs. Use variable substitution and an env file, or Docker secrets.

Apply within this block:

-      - NODELOC_OAUTH_ENABLED=true
-      - NODELOC_CLIENT_ID=your_client_id_here
-      - NODELOC_CLIENT_SECRET=your_client_secret_here
+      - NODELOC_OAUTH_ENABLED=${NODELOC_OAUTH_ENABLED:-false}
+      - NODELOC_CLIENT_ID=${NODELOC_CLIENT_ID}
+      - NODELOC_CLIENT_SECRET=${NODELOC_CLIENT_SECRET}

And add outside this hunk:

env_file:
  - .env

Optional (if supported by backend): expose minimum trust level to avoid 401 loops.

Run to confirm expected var names:


🏁 Script executed:

#!/bin/bash
rg -nC2 -P 'NODELOC_(MINIMUM_TRUST_LEVEL|OAUTH_ENABLED|CLIENT_ID|CLIENT_SECRET)|NodeLoc(MinimumTrustLevel|OAuthEnabled|ClientId|ClientSecret)' --type go --type ts --type tsx --type js

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Retry search without unsupported --type flags (fixes "unrecognized file type: tsx").
rg -nC2 -P 'NODELOC_(MINIMUM_TRUST_LEVEL|OAUTH_ENABLED|CLIENT_ID|CLIENT_SECRET)|NodeLoc(MinimumTrustLevel|OAuthEnabled|ClientId|ClientSecret)' || true
# Fallback using grep across the repo
grep -RIn --line-number -E 'NODELOC_(MINIMUM_TRUST_LEVEL|OAUTH_ENABLED|CLIENT_ID|CLIENT_SECRET)|NodeLoc(MinimumTrustLevel|OAuthEnabled|ClientId|ClientSecret)' . || true

Length of output: 11409


Don't hardcode NodeLoc OAuth credentials — use env vars or Docker secrets.

docker-compose.yml (lines 20–22) currently hardcodes NODELOC_OAUTH_ENABLED/CLIENT_ID/CLIENT_SECRET; these are consumed by the app (common.NodeLocClientId/Secret), so move them to env substitution or Docker secrets to prevent VCS/log leakage.

-      - NODELOC_OAUTH_ENABLED=true
-      - NODELOC_CLIENT_ID=your_client_id_here
-      - NODELOC_CLIENT_SECRET=your_client_secret_here
+      - NODELOC_OAUTH_ENABLED=${NODELOC_OAUTH_ENABLED:-false}
+      - NODELOC_CLIENT_ID=${NODELOC_CLIENT_ID}
+      - NODELOC_CLIENT_SECRET=${NODELOC_CLIENT_SECRET}

Add outside this service block:

env_file:
  - .env
🤖 Prompt for AI Agents
In docker-compose.yml around lines 19 to 22, replace the hardcoded
NODELOC_OAUTH_ENABLED, NODELOC_CLIENT_ID and NODELOC_CLIENT_SECRET entries with
references to externalized values: remove those three hardcoded lines and add an
env_file: - .env entry at the service level (or top-level depending on your
compose version) so the service reads NODELOC_* from a .env file, or
alternatively define them as Docker secrets and reference the secret names in
the service; ensure the .env file (or secrets) is excluded from VCS and that the
application still reads common.NodeLocClientId/Secret from the environment.

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: 6

♻️ Duplicate comments (4)
controller/nodeloc.go (4)

92-94: Fix redirect_uri path to match configured callback (and docs)

Your UI/docs indicate /oauth/nodeloc while code uses /api/oauth/nodeloc. OAuth requires exact match. Align the path, or update the router/docs—just make them identical.

-redirectURI := setting.ServerAddress + "/api/oauth/nodeloc"
+redirectURI := setting.ServerAddress + "/oauth/nodeloc"

If the API route truly is /api/oauth/nodeloc, update the admin instruction instead and keep code consistent.


57-60: Avoid panic on session ID extraction

id.(int) will panic if id is nil or not an int. Validate and fail gracefully.

 session := sessions.Default(c)
-id := session.Get("id")
-user.Id = id.(int)
+idVal := session.Get("id")
+userID, ok := idVal.(int)
+if !ok || userID == 0 {
+  c.JSON(http.StatusUnauthorized, gin.H{
+    "success": false,
+    "message": "未登录或会话无效",
+  })
+  return
+}
+user.Id = userID

154-163: Check userinfo HTTP status code and surface errors

You decode userinfo without verifying StatusCode. Return actionable errors on non-2xx and keep bodies readable for diagnostics.

 res2, err := client.Do(req)
 if err != nil {
   return nil, errors.New("failed to get user info from NodeLoc")
 }
 defer res2.Body.Close()
 
-var nodeLocUser NodeLocUser
-if err := json.NewDecoder(res2.Body).Decode(&nodeLocUser); err != nil {
-  return nil, err
-}
+body2, err := io.ReadAll(res2.Body)
+if err != nil {
+  return nil, fmt.Errorf("failed to read userinfo body: %v", err)
+}
+if res2.StatusCode < 200 || res2.StatusCode >= 300 {
+  return nil, fmt.Errorf("userinfo request failed with status %d: %s", res2.StatusCode, string(body2))
+}
+var nodeLocUser NodeLocUser
+if err := json.Unmarshal(body2, &nodeLocUser); err != nil {
+  return nil, err
+}

243-247: Avoid panic on “aff” session value

Type-assert “aff” safely before using it.

-affCode := session.Get("aff")
-inviterId := 0
-if affCode != nil {
-  inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
-}
+affRaw := session.Get("aff")
+inviterId := 0
+if s, ok := affRaw.(string); ok && s != "" {
+  inviterId, _ = model.GetUserIdByAffCode(s)
+}
🧹 Nitpick comments (3)
controller/nodeloc.go (3)

100-107: Propagate request context to outbound HTTP calls

Tie provider requests to the incoming request lifecycle; cancel them if the client disconnects.

-req, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(data.Encode()))
+req, err := http.NewRequestWithContext(c.Request.Context(), "POST", tokenEndpoint, strings.NewReader(data.Encode()))
 ...
-req, err = http.NewRequest("GET", userEndpoint, nil)
+req, err = http.NewRequestWithContext(c.Request.Context(), userEndpoint, nil)

Also applies to: 147-153


86-87: Externalize provider endpoints to config

Hard-coded NodeLoc endpoints make environment switches harder. Move https://conn.nodeloc.cc/oauth2/{token,userinfo} to config (e.g., common or setting) and reference them here.

Also applies to: 146-147


92-94: Verify ServerAddress correctness for redirect_uri

setting.ServerAddress is currently "http://localhost:3000" (frontend). Ensure this resolves to the publicly reachable host that serves the callback route, otherwise the provider will call the wrong origin.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fcf9ef6 and da50b23.

📒 Files selected for processing (2)
  • controller/nodeloc.go (1 hunks)
  • web/src/helpers/api.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/helpers/api.js
🧰 Additional context used
🧬 Code graph analysis (1)
controller/nodeloc.go (4)
common/constants.go (6)
  • NodeLocOAuthEnabled (47-47)
  • NodeLocClientId (89-89)
  • NodeLocClientSecret (90-90)
  • RegisterEnabled (51-51)
  • RoleCommonUser (139-139)
  • UserStatusEnabled (179-179)
common/gin.go (1)
  • ApiError (95-100)
model/user.go (4)
  • User (19-49)
  • IsNodeLocIdAlreadyTaken (911-915)
  • GetMaxUserId (181-185)
  • GetUserIdByAffCode (302-309)
setting/system_setting.go (1)
  • ServerAddress (3-3)

Comment on lines +45 to +48
user := model.User{
NodeLocId: nodeLocUser.Sub,
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make nodeloc_id unique at the DB layer to eliminate race conditions

There’s no unique constraint on nodeloc_id; concurrent binds can slip through the pre-check. Add a unique index and backfill migration.

Example change in model/user.go:

- NodeLocId        string         `json:"nodeloc_id" gorm:"column:nodeloc_id;index"`
+ NodeLocId        string         `json:"nodeloc_id" gorm:"column:nodeloc_id;uniqueIndex"`

Then add a safe migration to create the unique index and clean dupes.

Also applies to: 67-73

🤖 Prompt for AI Agents
In controller/nodeloc.go around lines 45-48 and 67-73, the code assigns
NodeLocId without a DB-level uniqueness guarantee which allows race conditions;
add a unique constraint/index on nodeloc_id in the User model (e.g.
tag/annotation in model/user.go) and implement a safe migration that 1) finds
and consolidates or removes duplicate rows (backfill) and 2) creates the unique
index atomically (or with CONCURRENTLY if supported), and update bind logic to
gracefully handle unique-violation errors (retry/return appropriate error) so
concurrent binds cannot create duplicates.

Comment on lines +49 to +55
if model.IsNodeLocIdAlreadyTaken(user.NodeLocId) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该 NodeLoc 账户已被绑定",
})
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Move “already taken” check after loading current user; allow idempotent self-binding

Checking “taken” before knowing who is logged-in prevents re-binding to self and masks ownership checks. Remove this early check and perform it after FillUserById().

- if model.IsNodeLocIdAlreadyTaken(user.NodeLocId) {
-   c.JSON(http.StatusOK, gin.H{
-     "success": false,
-     "message": "该 NodeLoc 账户已被绑定",
-   })
-   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
if model.IsNodeLocIdAlreadyTaken(user.NodeLocId) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该 NodeLoc 账户已被绑定",
})
return
}
🤖 Prompt for AI Agents
In controller/nodeloc.go around lines 49–55, the IsNodeLocIdAlreadyTaken check
is done before loading the current user which prevents idempotent self-binding
and hides ownership; move this check to after calling FillUserById() so you have
the current user's ID, then if IsNodeLocIdAlreadyTaken(user.NodeLocId) is true
verify whether the owner is the same user (allow if it is) and only return the
"already taken" response when the NodeLocId belongs to a different user; ensure
you call FillUserById() first and use that result to compare ownership before
rejecting.

Comment on lines +61 to +66
err = user.FillUserById()
if err != nil {
common.ApiError(c, err)
return
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enforce ownership: block binding to another account, allow no-op when already bound to self

After loading the current user, verify whether the NodeLocId belongs to someone else; if it belongs to this user, return success to make the operation idempotent.

 err = user.FillUserById()
 if err != nil {
   common.ApiError(c, err)
   return
 }
 
+ // Prevent binding to another user's NodeLoc and allow idempotent self-bind
+ if model.IsNodeLocIdAlreadyTaken(user.NodeLocId) {
+   existing := model.User{NodeLocId: user.NodeLocId}
+   if err := existing.FillUserByNodeLocId(); err == nil && existing.Id != 0 && existing.Id != user.Id {
+     c.JSON(http.StatusOK, gin.H{
+       "success": false,
+       "message": "该 NodeLoc 账户已被其它用户绑定",
+     })
+     return
+   }
+   // Already bound to current user
+   c.JSON(http.StatusOK, gin.H{
+     "success": true,
+     "message": "已绑定",
+   })
+   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
err = user.FillUserById()
if err != nil {
common.ApiError(c, err)
return
}
err = user.FillUserById()
if err != nil {
common.ApiError(c, err)
return
}
// Prevent binding to another user's NodeLoc and allow idempotent self-bind
if model.IsNodeLocIdAlreadyTaken(user.NodeLocId) {
existing := model.User{NodeLocId: user.NodeLocId}
if err := existing.FillUserByNodeLocId(); err == nil && existing.Id != 0 && existing.Id != user.Id {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "该 NodeLoc 账户已被其它用户绑定",
})
return
}
// Already bound to current user
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "已绑定",
})
return
}
🤖 Prompt for AI Agents
In controller/nodeloc.go around lines 61-66, after successfully calling
user.FillUserById(), load the NodeLoc record for the NodeLocId in the request
and enforce ownership: if the NodeLoc does not exist return not-found, if its
OwnerId equals the current user's ID return success immediately (no-op
idempotent behavior), and if its OwnerId is set to a different user return a
forbidden error; otherwise allow the original binding logic to proceed. Ensure
you use the existing API error helpers (common.ApiError or appropriate) and keep
responses consistent with other handlers.

Comment on lines +85 to +91
// Get access token using Basic Authentication as per NodeLoc documentation
tokenEndpoint := "https://conn.nodeloc.cc/oauth2/token"

// Create Basic Auth header: Base64(client_id:client_secret)
credentials := common.NodeLocClientId + ":" + common.NodeLocClientSecret
basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against missing client credentials

Avoid making external calls when client_id/secret are unset; return a clear configuration error.

 // Get access token using Basic Authentication as per NodeLoc documentation
 tokenEndpoint := "https://conn.nodeloc.cc/oauth2/token"
 
 // Create Basic Auth header: Base64(client_id:client_secret)
+if common.NodeLocClientId == "" || common.NodeLocClientSecret == "" {
+  return nil, errors.New("NodeLoc OAuth 未配置:client_id 或 client_secret 为空")
+}
 credentials := common.NodeLocClientId + ":" + common.NodeLocClientSecret
 basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
📝 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
// Get access token using Basic Authentication as per NodeLoc documentation
tokenEndpoint := "https://conn.nodeloc.cc/oauth2/token"
// Create Basic Auth header: Base64(client_id:client_secret)
credentials := common.NodeLocClientId + ":" + common.NodeLocClientSecret
basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
// Get access token using Basic Authentication as per NodeLoc documentation
tokenEndpoint := "https://conn.nodeloc.cc/oauth2/token"
// Create Basic Auth header: Base64(client_id:client_secret)
if common.NodeLocClientId == "" || common.NodeLocClientSecret == "" {
return nil, errors.New("NodeLoc OAuth 未配置:client_id 或 client_secret 为空")
}
credentials := common.NodeLocClientId + ":" + common.NodeLocClientSecret
basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
🤖 Prompt for AI Agents
In controller/nodeloc.go around lines 85 to 91, the code builds a Basic Auth
header using common.NodeLocClientId and common.NodeLocClientSecret without
validating they are set; add an early guard that checks both values are
non-empty (and optionally not the placeholder), and if either is missing return
a clear configuration error (e.g., wrap or return fmt.Errorf("nodeloc: missing
client_id or client_secret configuration")) before attempting any external HTTP
call or encoding; ensure callers receive this error so no outbound request is
made with empty credentials.

Comment on lines +186 to +192
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
})
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden state validation and clear it to prevent replay

Avoid type-assertion panic and delete state after use.

-state := c.Query("state")
-if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
+state := c.Query("state")
+sessStateRaw := session.Get("oauth_state")
+sessState, ok := sessStateRaw.(string)
+if state == "" || !ok || state != sessState {
   c.JSON(http.StatusForbidden, gin.H{
     "success": false,
     "message": "state is empty or not same",
   })
   return
 }
+session.Delete("oauth_state")
+_ = session.Save()
📝 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
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
})
return
}
state := c.Query("state")
sessStateRaw := session.Get("oauth_state")
sessState, ok := sessStateRaw.(string)
if state == "" || !ok || state != sessState {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "state is empty or not same",
})
return
}
session.Delete("oauth_state")
_ = session.Save()
🤖 Prompt for AI Agents
In controller/nodeloc.go around lines 186 to 192, the current state validation
uses a direct type assertion and never clears the stored state; update the logic
to safely retrieve the stored state with a checked type assertion (e.g., val, ok
:= session.Get("oauth_state").(string)) and treat missing/empty/unequal values
as forbidden, and after a successful match remove the oauth_state from the
session (session.Delete("oauth_state") and persist/save the session) to prevent
replay; ensure you handle the case where session.Get returns nil without
panicking.

Comment on lines 237 to 256
if common.RegisterEnabled {
user.Username = "nodeloc_" + strconv.Itoa(model.GetMaxUserId()+1)
user.DisplayName = nodeLocUser.Username
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled

affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
}

if err := user.Insert(inviterId); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Username generation can exceed 12 chars and collide; derive a safe, unique value

"nodeloc_"+GetMaxUserId()+1 risks length > 12 (violates validate:"max=12") and race/collision. Derive from provider username or a compact suffix, clamp to 12, and retry on unique constraint.

 if common.RegisterEnabled {
-  user.Username = "nodeloc_" + strconv.Itoa(model.GetMaxUserId()+1)
+  // Prefer provider username; fallback to compact base36 suffix; clamp to 12 chars
+  candidate := nodeLocUser.Username
+  if candidate == "" {
+    candidate = "nl_" + strings.ToLower(strconv.FormatInt(time.Now().UnixNano(), 36))
+  }
+  if len([]rune(candidate)) > 12 {
+    candidate = string([]rune(candidate)[:12])
+  }
+  user.Username = candidate
   user.DisplayName = nodeLocUser.Username
   user.Role = common.RoleCommonUser
   user.Status = common.UserStatusEnabled
@@
-  if err := user.Insert(inviterId); err != nil {
+  if err := user.Insert(inviterId); err != nil {
     c.JSON(http.StatusOK, gin.H{
       "success": false,
       "message": err.Error(),
     })
     return
   }

Follow-up: If Insert fails with duplicate username, retry by appending a short base36 suffix to stay within 12 chars. I can provide a small retry helper if you want.

📝 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
if common.RegisterEnabled {
user.Username = "nodeloc_" + strconv.Itoa(model.GetMaxUserId()+1)
user.DisplayName = nodeLocUser.Username
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
}
if err := user.Insert(inviterId); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {
if common.RegisterEnabled {
// Prefer provider username; fallback to compact base36 suffix; clamp to 12 chars
candidate := nodeLocUser.Username
if candidate == "" {
candidate = "nl_" + strings.ToLower(strconv.FormatInt(time.Now().UnixNano(), 36))
}
if len([]rune(candidate)) > 12 {
candidate = string([]rune(candidate)[:12])
}
user.Username = candidate
user.DisplayName = nodeLocUser.Username
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
affCode := session.Get("aff")
inviterId := 0
if affCode != nil {
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
}
if err := user.Insert(inviterId); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {

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: 0

♻️ Duplicate comments (7)
controller/nodeloc.go (7)

47-53: Move “already taken” check after loading current user; enforce ownership and idempotency

Checking before loading the current user prevents self-rebind and masks ownership. Perform the check after FillUserById() and allow no-op when already bound to self; block binding to another account.

-	if model.IsNodeLocIdAlreadyTaken(user.NodeLocId) {
-		c.JSON(http.StatusOK, gin.H{
-			"success": false,
-			"message": "该 NodeLoc 账户已被绑定",
-		})
-		return
-	}

Insert after successful FillUserById():

+	// Enforce ownership: allow idempotent self-binding, block binding to others
+	if model.IsNodeLocIdAlreadyTaken(user.NodeLocId) {
+		existing := model.User{NodeLocId: user.NodeLocId}
+		if err := existing.FillUserByNodeLocId(); err == nil && existing.Id != 0 {
+			if existing.Id != user.Id {
+				c.JSON(http.StatusOK, gin.H{
+					"success": false,
+					"message": "该 NodeLoc 账户已被其它用户绑定",
+				})
+				return
+			}
+			c.JSON(http.StatusOK, gin.H{
+				"success": true,
+				"message": "已绑定",
+			})
+			return
+		}
+	}

55-58: Harden session ID extraction to avoid panic and unauthorized access

Direct id.(int) will panic or accept zero. Validate and fail gracefully.

-	id := session.Get("id")
-	user.Id = id.(int)
+	idVal := session.Get("id")
+	userID, ok := idVal.(int)
+	if !ok || userID == 0 {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": "未登录或会话无效",
+		})
+		return
+	}
+	user.Id = userID

65-70: Make nodeloc_id unique at the DB layer; handle unique-violation on update

Concurrent binds can race past the pre-check. Add a DB unique index and handle conflicts.

Outside this file (model/user.go):

-	NodeLocId        string         `json:"nodeloc_id" gorm:"column:nodeloc_id;index"`
+	NodeLocId        string         `json:"nodeloc_id" gorm:"column:nodeloc_id;uniqueIndex"`

Migration: backfill by removing/merging dupes, then create the unique index (use CONCURRENTLY if supported). In this handler, map unique-violation to a friendly error.


86-89: Guard against missing client credentials before external calls

Avoid sending empty Basic credentials; return a clear config error.

-	credentials := common.NodeLocClientId + ":" + common.NodeLocClientSecret
+	if common.NodeLocClientId == "" || common.NodeLocClientSecret == "" {
+		return nil, errors.New("NodeLoc OAuth 未配置:client_id 或 client_secret 为空")
+	}
+	credentials := common.NodeLocClientId + ":" + common.NodeLocClientSecret

193-200: Fix state validation panic and clear state to prevent replay

Avoid type-assertion panic; delete oauth_state after successful match.

-	state := c.Query("state")
-	if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
+	state := c.Query("state")
+	sessStateRaw := session.Get("oauth_state")
+	sessState, ok := sessStateRaw.(string)
+	if state == "" || !ok || state != sessState {
 		c.JSON(http.StatusForbidden, gin.H{
 			"success": false,
 			"message": "state is empty or not same",
 		})
 		return
 	}
+	session.Delete("oauth_state")
+	_ = session.Save()

162-171: Check userinfo HTTP status and surface provider errors

You decode without checking res2.StatusCode. Return a clear error on non-2xx and avoid partial decodes.

-	res2, err := client.Do(req)
+	res2, err := client.Do(req)
 	if err != nil {
 		return nil, errors.New("failed to get user info from NodeLoc")
 	}
 	defer res2.Body.Close()
-
-	var nodeLocUser NodeLocUser
-	if err := json.NewDecoder(res2.Body).Decode(&nodeLocUser); err != nil {
-		return nil, err
-	}
+	body2, err := io.ReadAll(res2.Body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read userinfo body: %v", err)
+	}
+	if res2.StatusCode < 200 || res2.StatusCode >= 300 {
+		return nil, fmt.Errorf("userinfo request failed with status %d: %s", res2.StatusCode, string(body2))
+	}
+	var nodeLocUser NodeLocUser
+	if err := json.Unmarshal(body2, &nodeLocUser); err != nil {
+		return nil, fmt.Errorf("failed to parse userinfo: %v, body: %s", err, string(body2))
+	}

245-264: Username generation can violate max length and collide; derive a safe unique value

"NL_"+username may exceed 12 chars and collide. Clamp length and add a short suffix on conflict.

-			user.Username = "NL_" + nodeLocUser.Username
+			// Derive from provider username; clamp to 12 and ensure uniqueness
+			candidate := "NL_" + strings.TrimSpace(nodeLocUser.Username)
+			runes := []rune(candidate)
+			if len(runes) > 12 {
+				candidate = string(runes[:12])
+			}
+			user.Username = candidate
@@
-			if err := user.Insert(inviterId); err != nil {
+			if err := user.Insert(inviterId); err != nil {
+				// Retry once with a compact base36 suffix if duplicate
+				nowSuffix := strings.ToLower(strconv.FormatInt(time.Now().UnixNano(), 36))
+				// keep total <= 12
+				base := []rune(user.Username)
+				maxBase := 12 - 1 - 4 // "_" + 4-char suffix
+				if len(base) > maxBase {
+					base = base[:maxBase]
+				}
+				user.Username = string(base) + "_" + nowSuffix[:4]
+				if err2 := user.Insert(inviterId); err2 != nil {
+					c.JSON(http.StatusOK, gin.H{
+						"success": false,
+						"message": err2.Error(),
+					})
+					return
+				}
+				// success on retry
+				goto created
+			}
+		created:

Add missing import:

 import (
@@
 	"strings"
+	"strconv"
 	"time"
🧹 Nitpick comments (1)
controller/nodeloc.go (1)

251-255: Avoid aff code type-assertion panic

Safely extract aff from session.

-			affCode := session.Get("aff")
-			inviterId := 0
-			if affCode != nil {
-				inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
-			}
+			inviterId := 0
+			if affRaw, ok := session.Get("aff").(string); ok && affRaw != "" {
+				inviterId, _ = model.GetUserIdByAffCode(affRaw)
+			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da50b23 and d3e3fda.

📒 Files selected for processing (2)
  • controller/nodeloc.go (1 hunks)
  • web/src/helpers/api.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/helpers/api.js
🧰 Additional context used
🧬 Code graph analysis (1)
controller/nodeloc.go (3)
common/constants.go (6)
  • NodeLocOAuthEnabled (47-47)
  • NodeLocClientId (89-89)
  • NodeLocClientSecret (90-90)
  • RegisterEnabled (51-51)
  • RoleCommonUser (139-139)
  • UserStatusEnabled (179-179)
common/gin.go (1)
  • ApiError (95-100)
model/user.go (3)
  • User (19-49)
  • IsNodeLocIdAlreadyTaken (911-915)
  • GetUserIdByAffCode (302-309)
🔇 Additional comments (3)
controller/nodeloc.go (3)

117-133: Good: token endpoint error handling is robust

Nice: timeout is set, body is buffered for diagnostics, HTTP status is checked, and provider error fields are surfaced.


96-102: Verify callback path consistency across router, docs, and UI

controller/nodeloc.go (lines 96–102) builds redirect_uri as :///oauth/nodeloc — confirm your router, documentation, and frontend use that exact path (not /api/oauth/nodeloc); mismatches will cause the provider to reject with invalid_grant.


96-99: Only honor X-Forwarded-Host when running behind configured, trusted proxies

controller/nodeloc.go unconditionally uses X-Forwarded-Host to build the OAuth redirect_uri; that header can be spoofed by clients and lead to incorrect/malicious callback URLs.

File: controller/nodeloc.go — ~lines 96–99

host := c.Request.Host
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
	host = forwardedHost
}

Action (pick one):

  • Remove/avoid trusting X-Forwarded-Host and use c.Request.Host; or
  • Ensure gin's trusted proxies are explicitly configured at startup (engine.SetTrustedProxies([...])) so forwarded headers are only trusted from known proxies; or
  • Validate forwardedHost against an allowlist of expected hostnames before using it.

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: 0

Caution

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

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

324-329: Restrict nodeloc_id exposure — prevent unintended serialization

model.User declares NodeLocId with json:"nodeloc_id" (model/user.go:45) so any handler that returns a User or []User will serialize it. controller/user.go explicitly includes "nodeloc_id": user.NodeLocId (controller/user.go:463) and also returns whole user objects (c.JSON(..., "data": user) — see ~controller/user.go:324-329). Verify and sanitize list/admin endpoints (GetAllUsers — controller/user.go:274, SearchUsers — controller/user.go:289) so nodeloc_id is only returned from intended endpoints (e.g., GetSelf). Fix by removing/hiding the model JSON tag (e.g., json:"-") or using explicit response DTOs that exclude nodeloc_id, and remove the explicit field usage at controller/user.go:463.

🧹 Nitpick comments (1)
controller/user.go (1)

446-468: Return null for “unbound” to avoid ambiguous 0/empty values

If user.NodeLocId uses zero/empty as “not bound,” return null to the client to simplify UI logic and prevent treating 0 as a real ID.

   // 构建响应数据,包含用户信息和权限
-  responseData := map[string]interface{}{
+  // 标准化 NodeLoc 绑定态:未绑定返回 null
+  var nodeLocId interface{}
+  if user.NodeLocId == 0 || user.NodeLocId == "" {
+    nodeLocId = nil
+  } else {
+    nodeLocId = user.NodeLocId
+  }
+
+  responseData := map[string]interface{}{
     "id":                user.Id,
     "username":          user.Username,
     "display_name":      user.DisplayName,
     "role":              user.Role,
     "status":            user.Status,
     "email":             user.Email,
     "group":             user.Group,
     "quota":             user.Quota,
     "used_quota":        user.UsedQuota,
     "request_count":     user.RequestCount,
     "aff_code":          user.AffCode,
     "aff_count":         user.AffCount,
     "aff_quota":         user.AffQuota,
     "aff_history_quota": user.AffHistoryQuota,
     "inviter_id":        user.InviterId,
     "linux_do_id":       user.LinuxDOId,
-    "nodeloc_id":        user.NodeLocId,
+    "nodeloc_id":        nodeLocId,
     "setting":           user.Setting,
     "stripe_customer":   user.StripeCustomer,
     "sidebar_modules":   userSetting.SidebarModules, // 正确提取sidebar_modules字段
     "permissions":       permissions,                // 新增权限字段
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3e3fda and 6590b51.

📒 Files selected for processing (3)
  • controller/nodeloc.go (1 hunks)
  • controller/user.go (1 hunks)
  • web/src/components/settings/personal/cards/AccountManagement.jsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • controller/nodeloc.go
  • web/src/components/settings/personal/cards/AccountManagement.jsx
🔇 Additional comments (1)
controller/user.go (1)

463-463: LGTM: added NodeLoc ID to GetSelf payload

The field name nodeloc_id is consistent with existing snake_case keys (e.g., linux_do_id) and aligns with the PR’s NodeLoc flow.

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