-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
add nodeloc oauth2 login #1783
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
add nodeloc oauth2 login #1783
Conversation
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
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.
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. Comment |
There was a problem hiding this 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 loadUse 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 SVGConsumers 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 NLAuthEnabledMessage 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 ofSiLinux
.- 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 redirectAvoid 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 redirectPrevent 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 setterNo consumer reads
nodeLocOAuthEnabled
; this can go.- if (optionKey === 'NodeLocOAuthEnabled') { - setNodeLocOAuthEnabled(value); - }
497-516
: Consider supporting NodeLoc minimum trust level hereIf
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, andNodeLocUser.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 clarityResponding 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
📒 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 NodeLocIdsetupLogin (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 LGTMEndpoint and middleware align with existing OAuth routes.
web/src/App.jsx (1)
209-216
: Callback route LGTMMatches existing OAuth callback pattern with Suspense + Loading.
controller/misc.go (1)
54-56
: Status fields LGTMNames 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 importImporting
onNodeLocAuthClicked
is consistent with other providers.web/src/components/auth/RegisterForm.jsx (4)
46-52
: LGTM: NodeLoc wiringImports align with existing OAuth options and icon usage.
80-80
: LGTM: loading state
nodelocLoading
mirrors other provider states.
558-559
: LGTM: gating updatedIncluding
status.nodeloc_oauth
in the “other options” condition is correct.
654-655
: LGTM: primary/alt flow toggleThe added check ensures the email form shows when no OAuth is available.
web/src/components/auth/LoginForm.jsx (3)
46-52
: LGTM: NodeLoc importsHelper and icon imported consistently with other providers.
72-72
: LGTM: loading stateMirrors other provider loading flags.
647-648
: LGTM: flow toggle updatedIncluding
status.nodeloc_oauth
in the check is correct.web/src/components/settings/SystemSetting.jsx (1)
89-92
: LGTM: NodeLoc inputs addedState keys follow existing naming conventions.
session := sessions.Default(c) | ||
id := session.Get("id") | ||
user.Id = id.(int) | ||
|
||
err = user.FillUserById() | ||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
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) | ||
} |
There was a problem hiding this comment.
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
common.OptionMap["NLAuthEnabled"] = strconv.FormatBool(common.NodeLocOAuthEnabled) | ||
common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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"` |
There was a problem hiding this comment.
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.
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.
<stop stop-color="#009966"/> | ||
<stop offset="0.790909" stop-color="#FF9933"/> | ||
</linearGradient> |
There was a problem hiding this comment.
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.
<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.
There was a problem hiding this 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
📒 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 | 公开 | 微信扫码登录跳转 |
There was a problem hiding this 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
📒 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)
- name: Set up Docker Buildx | ||
uses: docker/setup-buildx-action@v3 | ||
|
There was a problem hiding this comment.
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.
- 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.
curl -O https://raw.githubusercontent.com/nodeloc/new-api/main/docker-compose.yml | ||
``` |
There was a problem hiding this comment.
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.
### 其他配置 | ||
```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 | ||
``` |
There was a problem hiding this comment.
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.
### 其他配置 | |
```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.
docker-compose.yml
Outdated
# NodeLoc OAuth2 配置 | ||
- NODELOC_OAUTH_ENABLED=true | ||
- NODELOC_CLIENT_ID=your_client_id_here | ||
- NODELOC_CLIENT_SECRET=your_client_secret_here |
There was a problem hiding this comment.
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.
There was a problem hiding this 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 ifid
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 errorsYou 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 valueType-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 callsTie 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 configHard-coded NodeLoc endpoints make environment switches harder. Move
https://conn.nodeloc.cc/oauth2/{token,userinfo}
to config (e.g.,common
orsetting
) 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
📒 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)
user := model.User{ | ||
NodeLocId: nodeLocUser.Sub, | ||
} | ||
|
There was a problem hiding this comment.
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.
if model.IsNodeLocIdAlreadyTaken(user.NodeLocId) { | ||
c.JSON(http.StatusOK, gin.H{ | ||
"success": false, | ||
"message": "该 NodeLoc 账户已被绑定", | ||
}) | ||
return | ||
} |
There was a problem hiding this comment.
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.
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.
err = user.FillUserById() | ||
if err != nil { | ||
common.ApiError(c, err) | ||
return | ||
} | ||
|
There was a problem hiding this comment.
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.
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.
// 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)) | ||
|
There was a problem hiding this comment.
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.
// 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.
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 | ||
} |
There was a problem hiding this comment.
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.
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.
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 { |
There was a problem hiding this comment.
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.
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 { |
There was a problem hiding this 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 idempotencyChecking 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 accessDirect
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 updateConcurrent 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 callsAvoid 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 replayAvoid 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 errorsYou 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 panicSafely 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
📒 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 robustNice: 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 UIcontroller/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 proxiescontroller/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.
There was a problem hiding this 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 serializationmodel.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 valuesIf
user.NodeLocId
uses zero/empty as “not bound,” returnnull
to the client to simplify UI logic and prevent treating0
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
📒 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 payloadThe field name
nodeloc_id
is consistent with existing snake_case keys (e.g.,linux_do_id
) and aligns with the PR’s NodeLoc flow.
Add Nodeloc Oauth2 login.
Summary by CodeRabbit
New Features
Changes
Chores
Documentation