Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
62856666
authored
Feb 23, 2026
by
Seefs
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: move user bindings to dedicated management modal
parent
2b3bfd4e
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
233 additions
and
67 deletions
+233
-67
controller/custom_oauth.go
+100
-21
controller/user.go
+38
-0
model/user.go
+31
-0
router/api-router.go
+3
-0
web/src/components/table/users/modals/EditUserModal.jsx
+61
-46
web/src/components/table/users/modals/UserBindingManagementModal.jsx
+0
-0
No files found.
controller/custom_oauth.go
View file @
62856666
...
...
@@ -38,6 +38,14 @@ type CustomOAuthProviderResponse struct {
AccessDeniedMessage
string
`json:"access_denied_message"`
}
type
UserOAuthBindingResponse
struct
{
ProviderId
int
`json:"provider_id"`
ProviderName
string
`json:"provider_name"`
ProviderSlug
string
`json:"provider_slug"`
ProviderIcon
string
`json:"provider_icon"`
ProviderUserId
string
`json:"provider_user_id"`
}
func
toCustomOAuthProviderResponse
(
p
*
model
.
CustomOAuthProvider
)
*
CustomOAuthProviderResponse
{
return
&
CustomOAuthProviderResponse
{
Id
:
p
.
Id
,
...
...
@@ -433,6 +441,30 @@ func DeleteCustomOAuthProvider(c *gin.Context) {
})
}
func
buildUserOAuthBindingsResponse
(
userId
int
)
([]
UserOAuthBindingResponse
,
error
)
{
bindings
,
err
:=
model
.
GetUserOAuthBindingsByUserId
(
userId
)
if
err
!=
nil
{
return
nil
,
err
}
response
:=
make
([]
UserOAuthBindingResponse
,
0
,
len
(
bindings
))
for
_
,
binding
:=
range
bindings
{
provider
,
err
:=
model
.
GetCustomOAuthProviderById
(
binding
.
ProviderId
)
if
err
!=
nil
{
continue
}
response
=
append
(
response
,
UserOAuthBindingResponse
{
ProviderId
:
binding
.
ProviderId
,
ProviderName
:
provider
.
Name
,
ProviderSlug
:
provider
.
Slug
,
ProviderIcon
:
provider
.
Icon
,
ProviderUserId
:
binding
.
ProviderUserId
,
})
}
return
response
,
nil
}
// GetUserOAuthBindings returns all OAuth bindings for the current user
func
GetUserOAuthBindings
(
c
*
gin
.
Context
)
{
userId
:=
c
.
GetInt
(
"id"
)
...
...
@@ -441,34 +473,43 @@ func GetUserOAuthBindings(c *gin.Context) {
return
}
bindings
,
err
:=
model
.
GetUserOAuthBindingsByUserId
(
userId
)
response
,
err
:=
buildUserOAuthBindingsResponse
(
userId
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
// Build response with provider info
type
BindingResponse
struct
{
ProviderId
int
`json:"provider_id"`
ProviderName
string
`json:"provider_name"`
ProviderSlug
string
`json:"provider_slug"`
ProviderIcon
string
`json:"provider_icon"`
ProviderUserId
string
`json:"provider_user_id"`
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
response
,
})
}
func
GetUserOAuthBindingsByAdmin
(
c
*
gin
.
Context
)
{
userIdStr
:=
c
.
Param
(
"id"
)
userId
,
err
:=
strconv
.
Atoi
(
userIdStr
)
if
err
!=
nil
{
common
.
ApiErrorMsg
(
c
,
"invalid user id"
)
return
}
response
:=
make
([]
BindingResponse
,
0
)
for
_
,
binding
:=
range
bindings
{
provider
,
err
:=
model
.
GetCustomOAuthProviderById
(
binding
.
ProviderId
)
if
err
!=
nil
{
continue
// Skip if provider not found
}
response
=
append
(
response
,
BindingResponse
{
ProviderId
:
binding
.
ProviderId
,
ProviderName
:
provider
.
Name
,
ProviderSlug
:
provider
.
Slug
,
ProviderIcon
:
provider
.
Icon
,
ProviderUserId
:
binding
.
ProviderUserId
,
})
targetUser
,
err
:=
model
.
GetUserById
(
userId
,
false
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
myRole
:=
c
.
GetInt
(
"role"
)
if
myRole
<=
targetUser
.
Role
&&
myRole
!=
common
.
RoleRootUser
{
common
.
ApiErrorMsg
(
c
,
"no permission"
)
return
}
response
,
err
:=
buildUserOAuthBindingsResponse
(
userId
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
...
...
@@ -503,3 +544,41 @@ func UnbindCustomOAuth(c *gin.Context) {
"message"
:
"解绑成功"
,
})
}
func
UnbindCustomOAuthByAdmin
(
c
*
gin
.
Context
)
{
userIdStr
:=
c
.
Param
(
"id"
)
userId
,
err
:=
strconv
.
Atoi
(
userIdStr
)
if
err
!=
nil
{
common
.
ApiErrorMsg
(
c
,
"invalid user id"
)
return
}
targetUser
,
err
:=
model
.
GetUserById
(
userId
,
false
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
myRole
:=
c
.
GetInt
(
"role"
)
if
myRole
<=
targetUser
.
Role
&&
myRole
!=
common
.
RoleRootUser
{
common
.
ApiErrorMsg
(
c
,
"no permission"
)
return
}
providerIdStr
:=
c
.
Param
(
"provider_id"
)
providerId
,
err
:=
strconv
.
Atoi
(
providerIdStr
)
if
err
!=
nil
{
common
.
ApiErrorMsg
(
c
,
"invalid provider id"
)
return
}
if
err
:=
model
.
DeleteUserOAuthBinding
(
userId
,
providerId
);
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"success"
,
})
}
controller/user.go
View file @
62856666
...
...
@@ -582,6 +582,44 @@ func UpdateUser(c *gin.Context) {
return
}
func
AdminClearUserBinding
(
c
*
gin
.
Context
)
{
id
,
err
:=
strconv
.
Atoi
(
c
.
Param
(
"id"
))
if
err
!=
nil
{
common
.
ApiErrorI18n
(
c
,
i18n
.
MsgInvalidParams
)
return
}
bindingType
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
c
.
Param
(
"binding_type"
)))
if
bindingType
==
""
{
common
.
ApiErrorI18n
(
c
,
i18n
.
MsgInvalidParams
)
return
}
user
,
err
:=
model
.
GetUserById
(
id
,
false
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
myRole
:=
c
.
GetInt
(
"role"
)
if
myRole
<=
user
.
Role
&&
myRole
!=
common
.
RoleRootUser
{
common
.
ApiErrorI18n
(
c
,
i18n
.
MsgUserNoPermissionSameLevel
)
return
}
if
err
:=
user
.
ClearBinding
(
bindingType
);
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
model
.
RecordLog
(
user
.
Id
,
model
.
LogTypeManage
,
fmt
.
Sprintf
(
"admin cleared %s binding for user %s"
,
bindingType
,
user
.
Username
))
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"success"
,
})
}
func
UpdateSelf
(
c
*
gin
.
Context
)
{
var
requestData
map
[
string
]
interface
{}
err
:=
json
.
NewDecoder
(
c
.
Request
.
Body
)
.
Decode
(
&
requestData
)
...
...
model/user.go
View file @
62856666
...
...
@@ -536,6 +536,37 @@ func (user *User) Edit(updatePassword bool) error {
return
updateUserCache
(
*
user
)
}
func
(
user
*
User
)
ClearBinding
(
bindingType
string
)
error
{
if
user
.
Id
==
0
{
return
errors
.
New
(
"user id is empty"
)
}
bindingColumnMap
:=
map
[
string
]
string
{
"email"
:
"email"
,
"github"
:
"github_id"
,
"discord"
:
"discord_id"
,
"oidc"
:
"oidc_id"
,
"wechat"
:
"wechat_id"
,
"telegram"
:
"telegram_id"
,
"linuxdo"
:
"linux_do_id"
,
}
column
,
ok
:=
bindingColumnMap
[
bindingType
]
if
!
ok
{
return
errors
.
New
(
"invalid binding type"
)
}
if
err
:=
DB
.
Model
(
&
User
{})
.
Where
(
"id = ?"
,
user
.
Id
)
.
Update
(
column
,
""
)
.
Error
;
err
!=
nil
{
return
err
}
if
err
:=
DB
.
Where
(
"id = ?"
,
user
.
Id
)
.
First
(
user
)
.
Error
;
err
!=
nil
{
return
err
}
return
updateUserCache
(
*
user
)
}
func
(
user
*
User
)
Delete
()
error
{
if
user
.
Id
==
0
{
return
errors
.
New
(
"id 为空!"
)
...
...
router/api-router.go
View file @
62856666
...
...
@@ -114,6 +114,9 @@ func SetApiRouter(router *gin.Engine) {
adminRoute
.
GET
(
"/topup"
,
controller
.
GetAllTopUps
)
adminRoute
.
POST
(
"/topup/complete"
,
controller
.
AdminCompleteTopUp
)
adminRoute
.
GET
(
"/search"
,
controller
.
SearchUsers
)
adminRoute
.
GET
(
"/:id/oauth/bindings"
,
controller
.
GetUserOAuthBindingsByAdmin
)
adminRoute
.
DELETE
(
"/:id/oauth/bindings/:provider_id"
,
controller
.
UnbindCustomOAuthByAdmin
)
adminRoute
.
DELETE
(
"/:id/bindings/:binding_type"
,
controller
.
AdminClearUserBinding
)
adminRoute
.
GET
(
"/:id"
,
controller
.
GetUser
)
adminRoute
.
POST
(
"/"
,
controller
.
CreateUser
)
adminRoute
.
POST
(
"/manage"
,
controller
.
ManageUser
)
...
...
web/src/components/table/users/modals/EditUserModal.jsx
View file @
62856666
...
...
@@ -45,7 +45,6 @@ import {
Avatar
,
Row
,
Col
,
Input
,
InputNumber
,
}
from
'@douyinfe/semi-ui'
;
import
{
...
...
@@ -56,6 +55,7 @@ import {
IconUserGroup
,
IconPlus
,
}
from
'@douyinfe/semi-icons'
;
import
UserBindingManagementModal
from
'./UserBindingManagementModal'
;
const
{
Text
,
Title
}
=
Typography
;
...
...
@@ -68,6 +68,7 @@ const EditUserModal = (props) => {
const
[
addAmountLocal
,
setAddAmountLocal
]
=
useState
(
''
);
const
isMobile
=
useIsMobile
();
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
bindingModalVisible
,
setBindingModalVisible
]
=
useState
(
false
);
const
formApiRef
=
useRef
(
null
);
const
isEdit
=
Boolean
(
userId
);
...
...
@@ -81,6 +82,7 @@ const EditUserModal = (props) => {
discord_id
:
''
,
wechat_id
:
''
,
telegram_id
:
''
,
linux_do_id
:
''
,
email
:
''
,
quota
:
0
,
group
:
'default'
,
...
...
@@ -115,8 +117,17 @@ const EditUserModal = (props) => {
useEffect
(()
=>
{
loadUser
();
if
(
userId
)
fetchGroups
();
setBindingModalVisible
(
false
);
},
[
props
.
editingUser
.
id
]);
const
openBindingModal
=
()
=>
{
setBindingModalVisible
(
true
);
};
const
closeBindingModal
=
()
=>
{
setBindingModalVisible
(
false
);
};
/* ----------------------- submit ----------------------- */
const
submit
=
async
(
values
)
=>
{
setLoading
(
true
);
...
...
@@ -316,56 +327,51 @@ const EditUserModal = (props) => {
</
Card
>
)
}
{
/* 绑定信息 */
}
<
Card
className=
'!rounded-2xl shadow-sm border-0'
>
<
div
className=
'flex items-center mb-2'
>
<
Avatar
size=
'small'
color=
'purple'
className=
'mr-2 shadow-md'
>
<
IconLink
size=
{
16
}
/>
</
Avatar
>
<
div
>
<
Text
className=
'text-lg font-medium'
>
{
t
(
'绑定信息'
)
}
</
Text
>
<
div
className=
'text-xs text-gray-600'
>
{
t
(
'第三方账户绑定状态(只读)'
)
}
{
/* 绑定信息入口 */
}
{
userId
&&
(
<
Card
className=
'!rounded-2xl shadow-sm border-0'
>
<
div
className=
'flex items-center justify-between gap-3'
>
<
div
className=
'flex items-center min-w-0'
>
<
Avatar
size=
'small'
color=
'purple'
className=
'mr-2 shadow-md'
>
<
IconLink
size=
{
16
}
/>
</
Avatar
>
<
div
className=
'min-w-0'
>
<
Text
className=
'text-lg font-medium'
>
{
t
(
'绑定信息'
)
}
</
Text
>
<
div
className=
'text-xs text-gray-600'
>
{
t
(
'第三方账户绑定状态(只读)'
)
}
</
div
>
</
div
>
</
div
>
<
Button
type=
'primary'
theme=
'outline'
onClick=
{
openBindingModal
}
>
{
t
(
'修改绑定'
)
}
</
Button
>
</
div
>
</
div
>
<
Row
gutter=
{
12
}
>
{
[
'github_id'
,
'discord_id'
,
'oidc_id'
,
'wechat_id'
,
'email'
,
'telegram_id'
,
].
map
((
field
)
=>
(
<
Col
span=
{
24
}
key=
{
field
}
>
<
Form
.
Input
field=
{
field
}
label=
{
t
(
`已绑定的 ${field.replace('_id', '').toUpperCase()} 账户`
,
)
}
readonly
placeholder=
{
t
(
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
,
)
}
/>
</
Col
>
))
}
</
Row
>
</
Card
>
</
Card
>
)
}
</
div
>
)
}
</
Form
>
</
Spin
>
</
SideSheet
>
<
UserBindingManagementModal
visible=
{
bindingModalVisible
}
onCancel=
{
closeBindingModal
}
userId=
{
userId
}
isMobile=
{
isMobile
}
formApiRef=
{
formApiRef
}
/>
{
/* 添加额度模态框 */
}
<
Modal
centered
...
...
@@ -401,7 +407,10 @@ const EditUserModal = (props) => {
<
div
className=
'mb-3'
>
<
div
className=
'mb-1'
>
<
Text
size=
'small'
>
{
t
(
'金额'
)
}
</
Text
>
<
Text
size=
'small'
type=
'tertiary'
>
(
{
t
(
'仅用于换算,实际保存的是额度'
)
}
)
</
Text
>
<
Text
size=
'small'
type=
'tertiary'
>
{
' '
}
(
{
t
(
'仅用于换算,实际保存的是额度'
)
}
)
</
Text
>
</
div
>
<
InputNumber
prefix=
{
getCurrencyConfig
().
symbol
}
...
...
@@ -411,7 +420,9 @@ const EditUserModal = (props) => {
onChange=
{
(
val
)
=>
{
setAddAmountLocal
(
val
);
setAddQuotaLocal
(
val
!=
null
&&
val
!==
''
?
displayAmountToQuota
(
Math
.
abs
(
val
))
*
Math
.
sign
(
val
)
:
''
,
val
!=
null
&&
val
!==
''
?
displayAmountToQuota
(
Math
.
abs
(
val
))
*
Math
.
sign
(
val
)
:
''
,
);
}
}
style=
{
{
width
:
'100%'
}
}
...
...
@@ -430,7 +441,11 @@ const EditUserModal = (props) => {
setAddQuotaLocal
(
val
);
setAddAmountLocal
(
val
!=
null
&&
val
!==
''
?
Number
((
quotaToDisplayAmount
(
Math
.
abs
(
val
))
*
Math
.
sign
(
val
)).
toFixed
(
2
))
?
Number
(
(
quotaToDisplayAmount
(
Math
.
abs
(
val
))
*
Math
.
sign
(
val
)
).
toFixed
(
2
),
)
:
''
,
);
}
}
...
...
web/src/components/table/users/modals/UserBindingManagementModal.jsx
0 → 100644
View file @
62856666
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment