Commit 62856666 by Seefs

feat: move user bindings to dedicated management modal

parent 2b3bfd4e
...@@ -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 { if err != nil {
provider, err := model.GetCustomOAuthProviderById(binding.ProviderId) common.ApiError(c, err)
if err != nil { return
continue // Skip if provider not found }
}
response = append(response, BindingResponse{ myRole := c.GetInt("role")
ProviderId: binding.ProviderId, if myRole <= targetUser.Role && myRole != common.RoleRootUser {
ProviderName: provider.Name, common.ApiErrorMsg(c, "no permission")
ProviderSlug: provider.Slug, return
ProviderIcon: provider.Icon, }
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",
})
}
...@@ -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)
......
...@@ -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 为空!")
......
...@@ -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)
......
...@@ -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,56 +327,51 @@ const EditUserModal = (props) => { ...@@ -316,56 +327,51 @@ const EditUserModal = (props) => {
</Card> </Card>
)} )}
{/* 绑定信息 */} {/* 绑定信息入口 */}
<Card className='!rounded-2xl shadow-sm border-0'> {userId && (
<div className='flex items-center mb-2'> <Card className='!rounded-2xl shadow-sm border-0'>
<Avatar <div className='flex items-center justify-between gap-3'>
size='small' <div className='flex items-center min-w-0'>
color='purple' <Avatar
className='mr-2 shadow-md' size='small'
> color='purple'
<IconLink size={16} /> className='mr-2 shadow-md'
</Avatar> >
<div> <IconLink size={16} />
<Text className='text-lg font-medium'> </Avatar>
{t('绑定信息')} <div className='min-w-0'>
</Text> <Text className='text-lg font-medium'>
<div className='text-xs text-gray-600'> {t('绑定信息')}
{t('第三方账户绑定状态(只读)')} </Text>
<div className='text-xs text-gray-600'>
{t('第三方账户绑定状态(只读)')}
</div>
</div>
</div> </div>
<Button
type='primary'
theme='outline'
onClick={openBindingModal}
>
{t('修改绑定')}
</Button>
</div> </div>
</div> </Card>
)}
<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>
</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),
)
: '', : '',
); );
}} }}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment