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
6b19c845
authored
Feb 24, 2026
by
Calcium-Ion
Committed by
GitHub
Feb 24, 2026
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2993 from seefs001/feature/user-oauth-detail
feat: move user bindings to dedicated management modal
parents
e9fa2a44
3a954e1e
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
644 additions
and
68 deletions
+644
-68
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
+62
-47
web/src/components/table/users/modals/UserBindingManagementModal.jsx
+410
-0
No files found.
controller/custom_oauth.go
View file @
6b19c845
...
...
@@ -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 @
6b19c845
...
...
@@ -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 @
6b19c845
...
...
@@ -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 @
6b19c845
...
...
@@ -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 @
6b19c845
...
...
@@ -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
);
...
...
@@ -196,7 +207,7 @@ const EditUserModal = (props) => {
onSubmit=
{
submit
}
>
{
({
values
})
=>
(
<
div
className=
'p-2'
>
<
div
className=
'p-2
space-y-3
'
>
{
/* 基本信息 */
}
<
Card
className=
'!rounded-2xl shadow-sm border-0'
>
<
div
className=
'flex items-center mb-2'
>
...
...
@@ -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 @
6b19c845
/*
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
[
showBoundOnly
,
setShowBoundOnly
]
=
React
.
useState
(
true
);
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
;
setShowBoundOnly
(
true
);
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
boundCount
=
allBindingItems
.
filter
((
item
)
=>
Boolean
(
item
.
value
),
).
length
;
const
visibleBindingItems
=
showBoundOnly
?
allBindingItems
.
filter
((
item
)
=>
Boolean
(
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=
'max-h-[68vh] overflow-y-auto pr-1 pb-2'
>
<
div
className=
'flex items-center justify-between mb-4 gap-3 flex-wrap'
>
<
Checkbox
checked=
{
showBoundOnly
}
onChange=
{
(
e
)
=>
setShowBoundOnly
(
Boolean
(
e
.
target
.
checked
))
}
>
{
t
(
'仅显示已绑定'
)
}
</
Checkbox
>
<
Text
type=
'tertiary'
>
{
t
(
'已绑定'
)
}
{
boundCount
}
/
{
allBindingItems
.
length
}
</
Text
>
</
div
>
{
visibleBindingItems
.
length
===
0
?
(
<
Card
className=
'!rounded-xl border-dashed'
>
<
Text
type=
'tertiary'
>
{
t
(
'暂无已绑定项'
)
}
</
Text
>
</
Card
>
)
:
(
<
div
className=
'grid grid-cols-1 lg:grid-cols-2 gap-4'
>
{
visibleBindingItems
.
map
((
item
,
index
)
=>
{
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
(
'未启用'
);
const
shouldSpanTwoColsOnDesktop
=
visibleBindingItems
.
length
%
2
===
1
&&
index
===
visibleBindingItems
.
length
-
1
;
return
(
<
Card
key=
{
item
.
key
}
className=
{
`!rounded-xl ${shouldSpanTwoColsOnDesktop ? 'lg:col-span-2' : ''}`
}
>
<
div
className=
'flex items-center justify-between gap-3 min-h-[92px]'
>
<
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'
?
t
(
'内置'
)
:
t
(
'自定义'
)
}
</
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
>
)
}
</
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