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
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
216 additions
and
50 deletions
+216
-50
controller/custom_oauth.go
+98
-19
controller/user.go
+38
-0
model/user.go
+31
-0
router/api-router.go
+3
-0
web/src/components/table/users/modals/EditUserModal.jsx
+46
-31
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 {
...
@@ -38,6 +38,14 @@ type CustomOAuthProviderResponse struct {
AccessDeniedMessage
string
`json:"access_denied_message"`
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
{
func
toCustomOAuthProviderResponse
(
p
*
model
.
CustomOAuthProvider
)
*
CustomOAuthProviderResponse
{
return
&
CustomOAuthProviderResponse
{
return
&
CustomOAuthProviderResponse
{
Id
:
p
.
Id
,
Id
:
p
.
Id
,
...
@@ -433,6 +441,30 @@ func DeleteCustomOAuthProvider(c *gin.Context) {
...
@@ -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
// GetUserOAuthBindings returns all OAuth bindings for the current user
func
GetUserOAuthBindings
(
c
*
gin
.
Context
)
{
func
GetUserOAuthBindings
(
c
*
gin
.
Context
)
{
userId
:=
c
.
GetInt
(
"id"
)
userId
:=
c
.
GetInt
(
"id"
)
...
@@ -441,34 +473,43 @@ func GetUserOAuthBindings(c *gin.Context) {
...
@@ -441,34 +473,43 @@ func GetUserOAuthBindings(c *gin.Context) {
return
return
}
}
bindings
,
err
:=
model
.
GetUserOAuthBindingsByUserId
(
userId
)
response
,
err
:=
buildUserOAuthBindingsResponse
(
userId
)
if
err
!=
nil
{
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
common
.
ApiError
(
c
,
err
)
return
return
}
}
// Build response with provider info
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
type
BindingResponse
struct
{
"success"
:
true
,
ProviderId
int
`json:"provider_id"`
"message"
:
""
,
ProviderName
string
`json:"provider_name"`
"data"
:
response
,
ProviderSlug
string
`json:"provider_slug"`
})
ProviderIcon
string
`json:"provider_icon"`
}
ProviderUserId
string
`json:"provider_user_id"`
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
)
targetUser
,
err
:=
model
.
GetUserById
(
userId
,
false
)
for
_
,
binding
:=
range
bindings
{
provider
,
err
:=
model
.
GetCustomOAuthProviderById
(
binding
.
ProviderId
)
if
err
!=
nil
{
if
err
!=
nil
{
continue
// Skip if provider not found
common
.
ApiError
(
c
,
err
)
return
}
}
response
=
append
(
response
,
BindingResponse
{
ProviderId
:
binding
.
ProviderId
,
myRole
:=
c
.
GetInt
(
"role"
)
ProviderName
:
provider
.
Name
,
if
myRole
<=
targetUser
.
Role
&&
myRole
!=
common
.
RoleRootUser
{
ProviderSlug
:
provider
.
Slug
,
common
.
ApiErrorMsg
(
c
,
"no permission"
)
ProviderIcon
:
provider
.
Icon
,
return
ProviderUserId
:
binding
.
ProviderUserId
,
}
})
response
,
err
:=
buildUserOAuthBindingsResponse
(
userId
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
...
@@ -503,3 +544,41 @@ func UnbindCustomOAuth(c *gin.Context) {
...
@@ -503,3 +544,41 @@ func UnbindCustomOAuth(c *gin.Context) {
"message"
:
"解绑成功"
,
"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) {
...
@@ -582,6 +582,44 @@ func UpdateUser(c *gin.Context) {
return
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
)
{
func
UpdateSelf
(
c
*
gin
.
Context
)
{
var
requestData
map
[
string
]
interface
{}
var
requestData
map
[
string
]
interface
{}
err
:=
json
.
NewDecoder
(
c
.
Request
.
Body
)
.
Decode
(
&
requestData
)
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 {
...
@@ -536,6 +536,37 @@ func (user *User) Edit(updatePassword bool) error {
return
updateUserCache
(
*
user
)
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
{
func
(
user
*
User
)
Delete
()
error
{
if
user
.
Id
==
0
{
if
user
.
Id
==
0
{
return
errors
.
New
(
"id 为空!"
)
return
errors
.
New
(
"id 为空!"
)
...
...
router/api-router.go
View file @
62856666
...
@@ -114,6 +114,9 @@ func SetApiRouter(router *gin.Engine) {
...
@@ -114,6 +114,9 @@ func SetApiRouter(router *gin.Engine) {
adminRoute
.
GET
(
"/topup"
,
controller
.
GetAllTopUps
)
adminRoute
.
GET
(
"/topup"
,
controller
.
GetAllTopUps
)
adminRoute
.
POST
(
"/topup/complete"
,
controller
.
AdminCompleteTopUp
)
adminRoute
.
POST
(
"/topup/complete"
,
controller
.
AdminCompleteTopUp
)
adminRoute
.
GET
(
"/search"
,
controller
.
SearchUsers
)
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
.
GET
(
"/:id"
,
controller
.
GetUser
)
adminRoute
.
POST
(
"/"
,
controller
.
CreateUser
)
adminRoute
.
POST
(
"/"
,
controller
.
CreateUser
)
adminRoute
.
POST
(
"/manage"
,
controller
.
ManageUser
)
adminRoute
.
POST
(
"/manage"
,
controller
.
ManageUser
)
...
...
web/src/components/table/users/modals/EditUserModal.jsx
View file @
62856666
...
@@ -45,7 +45,6 @@ import {
...
@@ -45,7 +45,6 @@ import {
Avatar
,
Avatar
,
Row
,
Row
,
Col
,
Col
,
Input
,
InputNumber
,
InputNumber
,
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
import
{
...
@@ -56,6 +55,7 @@ import {
...
@@ -56,6 +55,7 @@ import {
IconUserGroup
,
IconUserGroup
,
IconPlus
,
IconPlus
,
}
from
'@douyinfe/semi-icons'
;
}
from
'@douyinfe/semi-icons'
;
import
UserBindingManagementModal
from
'./UserBindingManagementModal'
;
const
{
Text
,
Title
}
=
Typography
;
const
{
Text
,
Title
}
=
Typography
;
...
@@ -68,6 +68,7 @@ const EditUserModal = (props) => {
...
@@ -68,6 +68,7 @@ const EditUserModal = (props) => {
const
[
addAmountLocal
,
setAddAmountLocal
]
=
useState
(
''
);
const
[
addAmountLocal
,
setAddAmountLocal
]
=
useState
(
''
);
const
isMobile
=
useIsMobile
();
const
isMobile
=
useIsMobile
();
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
bindingModalVisible
,
setBindingModalVisible
]
=
useState
(
false
);
const
formApiRef
=
useRef
(
null
);
const
formApiRef
=
useRef
(
null
);
const
isEdit
=
Boolean
(
userId
);
const
isEdit
=
Boolean
(
userId
);
...
@@ -81,6 +82,7 @@ const EditUserModal = (props) => {
...
@@ -81,6 +82,7 @@ const EditUserModal = (props) => {
discord_id
:
''
,
discord_id
:
''
,
wechat_id
:
''
,
wechat_id
:
''
,
telegram_id
:
''
,
telegram_id
:
''
,
linux_do_id
:
''
,
email
:
''
,
email
:
''
,
quota
:
0
,
quota
:
0
,
group
:
'default'
,
group
:
'default'
,
...
@@ -115,8 +117,17 @@ const EditUserModal = (props) => {
...
@@ -115,8 +117,17 @@ const EditUserModal = (props) => {
useEffect
(()
=>
{
useEffect
(()
=>
{
loadUser
();
loadUser
();
if
(
userId
)
fetchGroups
();
if
(
userId
)
fetchGroups
();
setBindingModalVisible
(
false
);
},
[
props
.
editingUser
.
id
]);
},
[
props
.
editingUser
.
id
]);
const
openBindingModal
=
()
=>
{
setBindingModalVisible
(
true
);
};
const
closeBindingModal
=
()
=>
{
setBindingModalVisible
(
false
);
};
/* ----------------------- submit ----------------------- */
/* ----------------------- submit ----------------------- */
const
submit
=
async
(
values
)
=>
{
const
submit
=
async
(
values
)
=>
{
setLoading
(
true
);
setLoading
(
true
);
...
@@ -316,9 +327,11 @@ const EditUserModal = (props) => {
...
@@ -316,9 +327,11 @@ const EditUserModal = (props) => {
</
Card
>
</
Card
>
)
}
)
}
{
/* 绑定信息 */
}
{
/* 绑定信息入口 */
}
{
userId
&&
(
<
Card
className=
'!rounded-2xl shadow-sm border-0'
>
<
Card
className=
'!rounded-2xl shadow-sm border-0'
>
<
div
className=
'flex items-center mb-2'
>
<
div
className=
'flex items-center justify-between gap-3'
>
<
div
className=
'flex items-center min-w-0'
>
<
Avatar
<
Avatar
size=
'small'
size=
'small'
color=
'purple'
color=
'purple'
...
@@ -326,7 +339,7 @@ const EditUserModal = (props) => {
...
@@ -326,7 +339,7 @@ const EditUserModal = (props) => {
>
>
<
IconLink
size=
{
16
}
/>
<
IconLink
size=
{
16
}
/>
</
Avatar
>
</
Avatar
>
<
div
>
<
div
className=
'min-w-0'
>
<
Text
className=
'text-lg font-medium'
>
<
Text
className=
'text-lg font-medium'
>
{
t
(
'绑定信息'
)
}
{
t
(
'绑定信息'
)
}
</
Text
>
</
Text
>
...
@@ -335,37 +348,30 @@ const EditUserModal = (props) => {
...
@@ -335,37 +348,30 @@ const EditUserModal = (props) => {
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
<
Button
<
Row
gutter=
{
12
}
>
type=
'primary'
{
[
theme=
'outline'
'github_id'
,
onClick=
{
openBindingModal
}
'discord_id'
,
>
'oidc_id'
,
{
t
(
'修改绑定'
)
}
'wechat_id'
,
</
Button
>
'email'
,
</
div
>
'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
>
</
div
>
)
}
)
}
</
Form
>
</
Form
>
</
Spin
>
</
Spin
>
</
SideSheet
>
</
SideSheet
>
<
UserBindingManagementModal
visible=
{
bindingModalVisible
}
onCancel=
{
closeBindingModal
}
userId=
{
userId
}
isMobile=
{
isMobile
}
formApiRef=
{
formApiRef
}
/>
{
/* 添加额度模态框 */
}
{
/* 添加额度模态框 */
}
<
Modal
<
Modal
centered
centered
...
@@ -401,7 +407,10 @@ const EditUserModal = (props) => {
...
@@ -401,7 +407,10 @@ const EditUserModal = (props) => {
<
div
className=
'mb-3'
>
<
div
className=
'mb-3'
>
<
div
className=
'mb-1'
>
<
div
className=
'mb-1'
>
<
Text
size=
'small'
>
{
t
(
'金额'
)
}
</
Text
>
<
Text
size=
'small'
>
{
t
(
'金额'
)
}
</
Text
>
<
Text
size=
'small'
type=
'tertiary'
>
(
{
t
(
'仅用于换算,实际保存的是额度'
)
}
)
</
Text
>
<
Text
size=
'small'
type=
'tertiary'
>
{
' '
}
(
{
t
(
'仅用于换算,实际保存的是额度'
)
}
)
</
Text
>
</
div
>
</
div
>
<
InputNumber
<
InputNumber
prefix=
{
getCurrencyConfig
().
symbol
}
prefix=
{
getCurrencyConfig
().
symbol
}
...
@@ -411,7 +420,9 @@ const EditUserModal = (props) => {
...
@@ -411,7 +420,9 @@ const EditUserModal = (props) => {
onChange=
{
(
val
)
=>
{
onChange=
{
(
val
)
=>
{
setAddAmountLocal
(
val
);
setAddAmountLocal
(
val
);
setAddQuotaLocal
(
setAddQuotaLocal
(
val
!=
null
&&
val
!==
''
?
displayAmountToQuota
(
Math
.
abs
(
val
))
*
Math
.
sign
(
val
)
:
''
,
val
!=
null
&&
val
!==
''
?
displayAmountToQuota
(
Math
.
abs
(
val
))
*
Math
.
sign
(
val
)
:
''
,
);
);
}
}
}
}
style=
{
{
width
:
'100%'
}
}
style=
{
{
width
:
'100%'
}
}
...
@@ -430,7 +441,11 @@ const EditUserModal = (props) => {
...
@@ -430,7 +441,11 @@ const EditUserModal = (props) => {
setAddQuotaLocal
(
val
);
setAddQuotaLocal
(
val
);
setAddAmountLocal
(
setAddAmountLocal
(
val
!=
null
&&
val
!==
''
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