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
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
612 additions
and
50 deletions
+612
-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
+396
-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
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
React
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
API
,
showError
,
showSuccess
,
getOAuthProviderIcon
,
}
from
'../../../../helpers'
;
import
{
Modal
,
Spin
,
Typography
,
Card
,
Checkbox
,
Tag
,
Button
,
}
from
'@douyinfe/semi-ui'
;
import
{
IconLink
,
IconMail
,
IconDelete
,
IconGithubLogo
,
}
from
'@douyinfe/semi-icons'
;
import
{
SiDiscord
,
SiTelegram
,
SiWechat
,
SiLinux
}
from
'react-icons/si'
;
const
{
Text
}
=
Typography
;
const
UserBindingManagementModal
=
({
visible
,
onCancel
,
userId
,
isMobile
,
formApiRef
,
})
=>
{
const
{
t
}
=
useTranslation
();
const
[
bindingLoading
,
setBindingLoading
]
=
React
.
useState
(
false
);
const
[
showUnboundOnly
,
setShowUnboundOnly
]
=
React
.
useState
(
false
);
const
[
statusInfo
,
setStatusInfo
]
=
React
.
useState
({});
const
[
customOAuthBindings
,
setCustomOAuthBindings
]
=
React
.
useState
([]);
const
[
bindingActionLoading
,
setBindingActionLoading
]
=
React
.
useState
({});
const
loadBindingData
=
React
.
useCallback
(
async
()
=>
{
if
(
!
userId
)
return
;
setBindingLoading
(
true
);
try
{
const
[
statusRes
,
customBindingRes
]
=
await
Promise
.
all
([
API
.
get
(
'/api/status'
),
API
.
get
(
`/api/user/
${
userId
}
/oauth/bindings`
),
]);
if
(
statusRes
.
data
?.
success
)
{
setStatusInfo
(
statusRes
.
data
.
data
||
{});
}
else
{
showError
(
statusRes
.
data
?.
message
||
t
(
'操作失败'
));
}
if
(
customBindingRes
.
data
?.
success
)
{
setCustomOAuthBindings
(
customBindingRes
.
data
.
data
||
[]);
}
else
{
showError
(
customBindingRes
.
data
?.
message
||
t
(
'操作失败'
));
}
}
catch
(
error
)
{
showError
(
error
.
response
?.
data
?.
message
||
error
.
message
||
t
(
'操作失败'
),
);
}
finally
{
setBindingLoading
(
false
);
}
},
[
t
,
userId
]);
React
.
useEffect
(()
=>
{
if
(
!
visible
)
return
;
setShowUnboundOnly
(
false
);
setBindingActionLoading
({});
loadBindingData
();
},
[
visible
,
loadBindingData
]);
const
setBindingLoadingState
=
(
key
,
value
)
=>
{
setBindingActionLoading
((
prev
)
=>
({
...
prev
,
[
key
]:
value
}));
};
const
handleUnbindBuiltInAccount
=
(
bindingItem
)
=>
{
if
(
!
userId
)
return
;
Modal
.
confirm
({
title
:
t
(
'确认解绑'
),
content
:
t
(
'确定要解绑 {{name}} 吗?'
,
{
name
:
bindingItem
.
name
}),
okText
:
t
(
'确认'
),
cancelText
:
t
(
'取消'
),
onOk
:
async
()
=>
{
const
loadingKey
=
`builtin-
${
bindingItem
.
key
}
`
;
setBindingLoadingState
(
loadingKey
,
true
);
try
{
const
res
=
await
API
.
delete
(
`/api/user/
${
userId
}
/bindings/
${
bindingItem
.
key
}
`
,
);
if
(
!
res
.
data
?.
success
)
{
showError
(
res
.
data
?.
message
||
t
(
'操作失败'
));
return
;
}
formApiRef
.
current
?.
setValue
(
bindingItem
.
field
,
''
);
showSuccess
(
t
(
'解绑成功'
));
}
catch
(
error
)
{
showError
(
error
.
response
?.
data
?.
message
||
error
.
message
||
t
(
'操作失败'
),
);
}
finally
{
setBindingLoadingState
(
loadingKey
,
false
);
}
},
});
};
const
handleUnbindCustomOAuthAccount
=
(
provider
)
=>
{
if
(
!
userId
)
return
;
Modal
.
confirm
({
title
:
t
(
'确认解绑'
),
content
:
t
(
'确定要解绑 {{name}} 吗?'
,
{
name
:
provider
.
name
}),
okText
:
t
(
'确认'
),
cancelText
:
t
(
'取消'
),
onOk
:
async
()
=>
{
const
loadingKey
=
`custom-
${
provider
.
id
}
`
;
setBindingLoadingState
(
loadingKey
,
true
);
try
{
const
res
=
await
API
.
delete
(
`/api/user/
${
userId
}
/oauth/bindings/
${
provider
.
id
}
`
,
);
if
(
!
res
.
data
?.
success
)
{
showError
(
res
.
data
?.
message
||
t
(
'操作失败'
));
return
;
}
setCustomOAuthBindings
((
prev
)
=>
prev
.
filter
(
(
item
)
=>
Number
(
item
.
provider_id
)
!==
Number
(
provider
.
id
),
),
);
showSuccess
(
t
(
'解绑成功'
));
}
catch
(
error
)
{
showError
(
error
.
response
?.
data
?.
message
||
error
.
message
||
t
(
'操作失败'
),
);
}
finally
{
setBindingLoadingState
(
loadingKey
,
false
);
}
},
});
};
const
currentValues
=
formApiRef
.
current
?.
getValues
?.()
||
{};
const
builtInBindingItems
=
[
{
key
:
'email'
,
field
:
'email'
,
name
:
t
(
'邮箱'
),
enabled
:
true
,
value
:
currentValues
.
email
,
icon
:
(
<
IconMail
size=
'default'
className=
'text-slate-600 dark:text-slate-300'
/>
),
},
{
key
:
'github'
,
field
:
'github_id'
,
name
:
'GitHub'
,
enabled
:
Boolean
(
statusInfo
.
github_oauth
),
value
:
currentValues
.
github_id
,
icon
:
(
<
IconGithubLogo
size=
'default'
className=
'text-slate-600 dark:text-slate-300'
/>
),
},
{
key
:
'discord'
,
field
:
'discord_id'
,
name
:
'Discord'
,
enabled
:
Boolean
(
statusInfo
.
discord_oauth
),
value
:
currentValues
.
discord_id
,
icon
:
(
<
SiDiscord
size=
{
20
}
className=
'text-slate-600 dark:text-slate-300'
/>
),
},
{
key
:
'oidc'
,
field
:
'oidc_id'
,
name
:
'OIDC'
,
enabled
:
Boolean
(
statusInfo
.
oidc_enabled
),
value
:
currentValues
.
oidc_id
,
icon
:
(
<
IconLink
size=
'default'
className=
'text-slate-600 dark:text-slate-300'
/>
),
},
{
key
:
'wechat'
,
field
:
'wechat_id'
,
name
:
t
(
'微信'
),
enabled
:
Boolean
(
statusInfo
.
wechat_login
),
value
:
currentValues
.
wechat_id
,
icon
:
(
<
SiWechat
size=
{
20
}
className=
'text-slate-600 dark:text-slate-300'
/>
),
},
{
key
:
'telegram'
,
field
:
'telegram_id'
,
name
:
'Telegram'
,
enabled
:
Boolean
(
statusInfo
.
telegram_oauth
),
value
:
currentValues
.
telegram_id
,
icon
:
(
<
SiTelegram
size=
{
20
}
className=
'text-slate-600 dark:text-slate-300'
/>
),
},
{
key
:
'linuxdo'
,
field
:
'linux_do_id'
,
name
:
'LinuxDO'
,
enabled
:
Boolean
(
statusInfo
.
linuxdo_oauth
),
value
:
currentValues
.
linux_do_id
,
icon
:
(
<
SiLinux
size=
{
20
}
className=
'text-slate-600 dark:text-slate-300'
/>
),
},
];
const
customBindingMap
=
new
Map
(
customOAuthBindings
.
map
((
item
)
=>
[
Number
(
item
.
provider_id
),
item
]),
);
const
customProviderMap
=
new
Map
(
(
statusInfo
.
custom_oauth_providers
||
[]).
map
((
provider
)
=>
[
Number
(
provider
.
id
),
provider
,
]),
);
customOAuthBindings
.
forEach
((
binding
)
=>
{
if
(
!
customProviderMap
.
has
(
Number
(
binding
.
provider_id
)))
{
customProviderMap
.
set
(
Number
(
binding
.
provider_id
),
{
id
:
binding
.
provider_id
,
name
:
binding
.
provider_name
,
icon
:
binding
.
provider_icon
,
});
}
});
const
customBindingItems
=
Array
.
from
(
customProviderMap
.
values
()).
map
(
(
provider
)
=>
{
const
binding
=
customBindingMap
.
get
(
Number
(
provider
.
id
));
return
{
key
:
`custom-
${
provider
.
id
}
`
,
providerId
:
provider
.
id
,
name
:
provider
.
name
,
enabled
:
true
,
value
:
binding
?.
provider_user_id
||
''
,
icon
:
getOAuthProviderIcon
(
provider
.
icon
||
binding
?.
provider_icon
||
''
,
20
,
),
};
},
);
const
allBindingItems
=
[
...
builtInBindingItems
.
map
((
item
)
=>
({
...
item
,
type
:
'builtin'
})),
...
customBindingItems
.
map
((
item
)
=>
({
...
item
,
type
:
'custom'
})),
];
const
visibleBindingItems
=
showUnboundOnly
?
allBindingItems
.
filter
((
item
)
=>
!
item
.
value
)
:
allBindingItems
;
return
(
<
Modal
centered
visible=
{
visible
}
onCancel=
{
onCancel
}
footer=
{
null
}
width=
{
isMobile
?
'100%'
:
760
}
title=
{
<
div
className=
'flex items-center'
>
<
IconLink
className=
'mr-2'
/>
{
t
(
'绑定信息'
)
}
</
div
>
}
>
<
Spin
spinning=
{
bindingLoading
}
>
<
div
className=
'flex items-center justify-between mb-4 gap-3 flex-wrap'
>
<
Checkbox
checked=
{
showUnboundOnly
}
onChange=
{
(
e
)
=>
setShowUnboundOnly
(
Boolean
(
e
.
target
.
checked
))
}
>
{
`${t('筛选')} ${t('未绑定')}`
}
</
Checkbox
>
<
Text
type=
'tertiary'
>
{
t
(
'筛选'
)
}
·
{
visibleBindingItems
.
length
}
</
Text
>
</
div
>
{
visibleBindingItems
.
length
===
0
?
(
<
Card
className=
'!rounded-xl border-dashed'
>
<
Text
type=
'tertiary'
>
{
t
(
'暂无自定义 OAuth 提供商'
)
}
</
Text
>
</
Card
>
)
:
(
<
div
className=
'grid grid-cols-1 lg:grid-cols-2 gap-3'
>
{
visibleBindingItems
.
map
((
item
)
=>
{
const
isBound
=
Boolean
(
item
.
value
);
const
loadingKey
=
item
.
type
===
'builtin'
?
`builtin-${item.key}`
:
`custom-${item.providerId}`
;
const
statusText
=
isBound
?
item
.
value
:
item
.
enabled
?
t
(
'未绑定'
)
:
t
(
'未启用'
);
return
(
<
Card
key=
{
item
.
key
}
className=
'!rounded-xl'
>
<
div
className=
'flex items-center justify-between gap-3'
>
<
div
className=
'flex items-center flex-1 min-w-0'
>
<
div
className=
'w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'
>
{
item
.
icon
}
</
div
>
<
div
className=
'min-w-0 flex-1'
>
<
div
className=
'font-medium text-gray-900 flex items-center gap-2'
>
<
span
>
{
item
.
name
}
</
span
>
<
Tag
size=
'small'
color=
'white'
>
{
item
.
type
===
'builtin'
?
'Built-in'
:
'Custom'
}
</
Tag
>
</
div
>
<
div
className=
'text-sm text-gray-500 truncate'
>
{
statusText
}
</
div
>
</
div
>
</
div
>
<
Button
type=
'danger'
theme=
'borderless'
icon=
{
<
IconDelete
/>
}
size=
'small'
disabled=
{
!
isBound
}
loading=
{
Boolean
(
bindingActionLoading
[
loadingKey
])
}
onClick=
{
()
=>
{
if
(
item
.
type
===
'builtin'
)
{
handleUnbindBuiltInAccount
(
item
);
return
;
}
handleUnbindCustomOAuthAccount
({
id
:
item
.
providerId
,
name
:
item
.
name
,
});
}
}
>
{
t
(
'解绑'
)
}
</
Button
>
</
div
>
</
Card
>
);
})
}
</
div
>
)
}
</
Spin
>
</
Modal
>
);
};
export
default
UserBindingManagementModal
;
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