You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@paimon.apache.org by ni...@apache.org on 2024/04/06 11:03:28 UTC

(paimon-webui) branch main updated: [Improvement] Improves the user management interface (#148)

This is an automated email from the ASF dual-hosted git repository.

nicholasjiang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-webui.git


The following commit(s) were added to refs/heads/main by this push:
     new a51f2bb  [Improvement]  Improves the user management interface (#148)
a51f2bb is described below

commit a51f2bb8f6f3f13ba195654e825f00ec9acbdddf
Author: s7monk <34...@users.noreply.github.com>
AuthorDate: Sat Apr 6 19:03:23 2024 +0800

    [Improvement]  Improves the user management interface (#148)
---
 .../web/server/controller/UserController.java      |  79 ++++++++-
 .../web/server/data/dto/UserWithRolesDTO.java      |  35 ++++
 .../apache/paimon/web/server/data/model/User.java  |   5 +
 .../web/server/data/result/enums/Status.java       |   2 +
 .../data/{model/User.java => vo/UserVO.java}       |  47 ++---
 .../paimon/web/server/mapper/UserMapper.java       |  14 +-
 .../paimon/web/server/service/UserService.java     |  53 +++++-
 .../web/server/service/impl/UserServiceImpl.java   |  96 +++++++++++
 .../src/main/resources/i18n/messages.properties    |   1 +
 .../main/resources/i18n/messages_en_US.properties  |   1 +
 .../main/resources/i18n/messages_zh_CN.properties  |   1 +
 .../src/main/resources/mapper/UserMapper.xml       |  58 ++++---
 .../web/server/controller/PermissionTest.java      |   4 +-
 .../web/server/controller/UserControllerTest.java  | 190 +++++++++++++++++++++
 14 files changed, 523 insertions(+), 63 deletions(-)

diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/UserController.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/UserController.java
index 6cb1ceb..30158f8 100644
--- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/UserController.java
+++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/controller/UserController.java
@@ -19,17 +19,29 @@
 package org.apache.paimon.web.server.controller;
 
 import org.apache.paimon.web.server.data.model.User;
+import org.apache.paimon.web.server.data.result.PageR;
 import org.apache.paimon.web.server.data.result.R;
+import org.apache.paimon.web.server.data.result.enums.Status;
+import org.apache.paimon.web.server.data.vo.UserVO;
 import org.apache.paimon.web.server.service.UserService;
+import org.apache.paimon.web.server.util.PageSupport;
 
 import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
 import static org.apache.paimon.web.server.data.result.enums.Status.USER_NOT_EXIST;
 
 /** User api controller. */
@@ -41,19 +53,76 @@ public class UserController {
     @Autowired private UserService userService;
 
     /**
-     * get user by id.
+     * Get user by id.
      *
      * @param id user-id
-     * @return {@link R} with {@link User}
+     * @return {@link R} with {@link UserVO}
      */
     @SaCheckPermission("system:user:query")
     @GetMapping("/{id}")
-    public R<User> getUserById(@PathVariable("id") Long id) {
-        User user = userService.getById(id);
+    public R<UserVO> getUser(@PathVariable("id") Integer id) {
+        UserVO user = userService.getUserById(id);
         if (user == null) {
             return R.failed(USER_NOT_EXIST);
         }
-        user.setPassword(null);
         return R.succeed(user);
     }
+
+    /**
+     * Get user views with pagination.
+     *
+     * @param user filter conditions
+     * @return paginated user view objects
+     */
+    @SaCheckPermission("system:user:list")
+    @GetMapping("/list")
+    public PageR<UserVO> listUsers(User user) {
+        IPage<User> page = PageSupport.startPage();
+        List<UserVO> list = userService.listUsers(page, user);
+        return PageR.<UserVO>builder().success(true).total(page.getTotal()).data(list).build();
+    }
+
+    /**
+     * Add a new user.
+     *
+     * @param user the user to be added, must not be null
+     * @return a {@code R<Void>} response indicating success or failure
+     */
+    @SaCheckPermission("system:user:add")
+    @PostMapping
+    public R<Void> add(@Validated @RequestBody User user) {
+        if (!userService.checkUserNameUnique(user)) {
+            return R.failed(Status.USER_NAME_ALREADY_EXISTS, user.getUsername());
+        }
+
+        return userService.insertUser(user) > 0 ? R.succeed() : R.failed();
+    }
+
+    /**
+     * Update an existing user's details.
+     *
+     * @param user the user with updated details, must not be null
+     * @return a {@code R<Void>} response indicating success or failure
+     */
+    @SaCheckPermission("system:user:update")
+    @PutMapping
+    public R<Void> update(@Validated @RequestBody User user) {
+        if (!userService.checkUserNameUnique(user)) {
+            return R.failed(Status.USER_NAME_ALREADY_EXISTS, user.getUsername());
+        }
+
+        return userService.updateUser(user) > 0 ? R.succeed() : R.failed();
+    }
+
+    /**
+     * Delete one or more users by user ID.
+     *
+     * @param userIds an array of user IDs to be deleted
+     * @return a {@code R<Void>} response indicating success or failure
+     */
+    @SaCheckPermission("system:user:delete")
+    @DeleteMapping("/{userIds}")
+    public R<Void> delete(@PathVariable Integer[] userIds) {
+        return userService.deleteUserByIds(userIds) > 0 ? R.succeed() : R.failed();
+    }
 }
diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/UserWithRolesDTO.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/UserWithRolesDTO.java
new file mode 100644
index 0000000..77d4208
--- /dev/null
+++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/dto/UserWithRolesDTO.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.data.dto;
+
+import org.apache.paimon.web.server.data.model.SysRole;
+import org.apache.paimon.web.server.data.model.User;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+/** DTO of UserWithRoles. */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class UserWithRolesDTO extends User {
+
+    private List<SysRole> roles;
+}
diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/User.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/User.java
index 3bbc2d7..9f3444a 100644
--- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/User.java
+++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/User.java
@@ -20,6 +20,7 @@ package org.apache.paimon.web.server.data.model;
 
 import org.apache.paimon.web.server.constant.Constants;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -58,6 +59,10 @@ public class User extends BaseModel {
     /** avatar url. */
     private String url;
 
+    /** role ids. */
+    @TableField(exist = false)
+    private Integer[] roleIds;
+
     public boolean isAdmin() {
         return isAdmin(this.getId());
     }
diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
index 9a28cef..0f6b314 100644
--- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
+++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/result/enums/Status.java
@@ -40,10 +40,12 @@ public enum Status {
     REQUEST_PARAMS_NOT_VALID_ERROR(4001, "invalid.request.parameter"),
     REQUEST_PARAMS_ERROR(4002, "request.parameter.error"),
 
+    /** ------------user-----------------. */
     USER_NOT_EXIST(10001, "user.not.exist"),
     USER_PASSWORD_ERROR(10002, "user.password.error"),
     USER_DISABLED_ERROR(10003, "user.is.disabled"),
     USER_NOT_BING_TENANT(10004, "user.not.bing.tenant"),
+    USER_NAME_ALREADY_EXISTS(10005, "user.name.exist"),
     /** ------------role-----------------. */
     ROLE_IN_USED(10101, "role.in.used"),
     ROLE_NAME_IS_EXIST(10102, "role.name.exist"),
diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/User.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/UserVO.java
similarity index 52%
copy from paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/User.java
copy to paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/UserVO.java
index 3bbc2d7..3f59429 100644
--- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/model/User.java
+++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/data/vo/UserVO.java
@@ -16,53 +16,42 @@
  * limitations under the License.
  */
 
-package org.apache.paimon.web.server.data.model;
+package org.apache.paimon.web.server.data.vo;
 
-import org.apache.paimon.web.server.constant.Constants;
+import org.apache.paimon.web.server.data.model.SysRole;
 
-import com.baomidou.mybatisplus.annotation.TableLogic;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Data;
-import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
 
-/** user table model. */
+import java.time.LocalDateTime;
+import java.util.List;
+
+/** VO of User. */
 @Data
-@EqualsAndHashCode(callSuper = true)
-public class User extends BaseModel {
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserVO {
 
-    private static final long serialVersionUID = 1L;
+    private Integer id;
 
-    /** username. */
     private String username;
 
-    /** password. */
-    private String password;
-
-    /** nickname. */
     private String nickname;
 
-    /** login type (0:LOCAL,1:LDAP). */
-    private Integer userType;
+    private String userType;
 
-    /** mobile phone. */
     private String mobile;
 
-    /** email. */
     private String email;
 
-    /** is enable. */
     private Boolean enabled;
 
-    /** is delete. */
-    @TableLogic private Boolean isDelete;
-
-    /** avatar url. */
-    private String url;
+    private LocalDateTime createTime;
 
-    public boolean isAdmin() {
-        return isAdmin(this.getId());
-    }
+    private LocalDateTime updateTime;
 
-    public static boolean isAdmin(Integer userId) {
-        return userId != null && Constants.ADMIN_ID == userId;
-    }
+    private List<SysRole> roles;
 }
diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/UserMapper.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/UserMapper.java
index a58b47c..7c7459d 100644
--- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/UserMapper.java
+++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/mapper/UserMapper.java
@@ -18,10 +18,13 @@
 
 package org.apache.paimon.web.server.mapper;
 
+import org.apache.paimon.web.server.data.dto.UserWithRolesDTO;
 import org.apache.paimon.web.server.data.model.User;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
 
@@ -32,9 +35,10 @@ public interface UserMapper extends BaseMapper<User> {
      * Query user list.
      *
      * @param user query params
+     * @param page paging params
      * @return user list
      */
-    List<User> selectUserList(User user);
+    List<UserWithRolesDTO> listUsers(IPage<User> page, @Param("user") User user);
 
     /**
      * Query user list by role ID.
@@ -61,10 +65,10 @@ public interface UserMapper extends BaseMapper<User> {
     User selectUserByUserName(String username);
 
     /**
-     * Query user info by user ID.
+     * Retrieves a user along with their roles based on the user's ID.
      *
-     * @param userId user ID
-     * @return user info
+     * @param userId the ID of the user to retrieve
+     * @return the UserWithRolesDTO containing user and role information
      */
-    User selectUserById(Integer userId);
+    UserWithRolesDTO selectUserWithRolesById(Integer userId);
 }
diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/UserService.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/UserService.java
index 626fc5c..1b637b5 100644
--- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/UserService.java
+++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/UserService.java
@@ -22,7 +22,9 @@ import org.apache.paimon.web.server.data.dto.LoginDTO;
 import org.apache.paimon.web.server.data.model.User;
 import org.apache.paimon.web.server.data.result.exception.BaseException;
 import org.apache.paimon.web.server.data.vo.UserInfoVO;
+import org.apache.paimon.web.server.data.vo.UserVO;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 
 import java.util.List;
@@ -31,7 +33,32 @@ import java.util.List;
 public interface UserService extends IService<User> {
 
     /**
-     * login by username and password.
+     * Get a user by ID.
+     *
+     * @param id the user ID
+     * @return the UserVO or null if not found
+     */
+    UserVO getUserById(Integer id);
+
+    /**
+     * Select users with pagination.
+     *
+     * @param page the pagination information
+     * @param user the filter criteria
+     * @return list of UserVO
+     */
+    List<UserVO> listUsers(IPage<User> page, User user);
+
+    /**
+     * Check if the username is unique.
+     *
+     * @param user the user to check
+     * @return true if unique, false otherwise
+     */
+    boolean checkUserNameUnique(User user);
+
+    /**
+     * Login by username and password.
      *
      * @param loginDTO login params
      * @return {@link String}
@@ -53,4 +80,28 @@ public interface UserService extends IService<User> {
      * @return user list
      */
     List<User> selectUnallocatedList(User user);
+
+    /**
+     * Insert a new user.
+     *
+     * @param user the user to be inserted
+     * @return the number of rows affected
+     */
+    int insertUser(User user);
+
+    /**
+     * Update an existing user.
+     *
+     * @param user the user with updated information
+     * @return the number of rows affected
+     */
+    int updateUser(User user);
+
+    /**
+     * Delete users by user ID.
+     *
+     * @param userIds the ids of the users to delete
+     * @return the number of rows affected
+     */
+    int deleteUserByIds(Integer[] userIds);
 }
diff --git a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/UserServiceImpl.java b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/UserServiceImpl.java
index 17828ab..83da9ff 100644
--- a/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/UserServiceImpl.java
+++ b/paimon-web-server/src/main/java/org/apache/paimon/web/server/service/impl/UserServiceImpl.java
@@ -19,6 +19,7 @@
 package org.apache.paimon.web.server.service.impl;
 
 import org.apache.paimon.web.server.data.dto.LoginDTO;
+import org.apache.paimon.web.server.data.dto.UserWithRolesDTO;
 import org.apache.paimon.web.server.data.enums.UserType;
 import org.apache.paimon.web.server.data.model.RoleMenu;
 import org.apache.paimon.web.server.data.model.SysMenu;
@@ -30,7 +31,9 @@ import org.apache.paimon.web.server.data.result.exception.user.UserDisabledExcep
 import org.apache.paimon.web.server.data.result.exception.user.UserNotExistsException;
 import org.apache.paimon.web.server.data.result.exception.user.UserPasswordNotMatchException;
 import org.apache.paimon.web.server.data.vo.UserInfoVO;
+import org.apache.paimon.web.server.data.vo.UserVO;
 import org.apache.paimon.web.server.mapper.UserMapper;
+import org.apache.paimon.web.server.mapper.UserRoleMapper;
 import org.apache.paimon.web.server.service.LdapService;
 import org.apache.paimon.web.server.service.RoleMenuService;
 import org.apache.paimon.web.server.service.SysMenuService;
@@ -38,17 +41,23 @@ import org.apache.paimon.web.server.service.SysRoleService;
 import org.apache.paimon.web.server.service.TenantService;
 import org.apache.paimon.web.server.service.UserRoleService;
 import org.apache.paimon.web.server.service.UserService;
+import org.apache.paimon.web.server.util.StringUtils;
 
 import cn.dev33.satoken.secure.SaSecureUtil;
 import cn.dev33.satoken.stp.StpUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
 /** UserServiceImpl. */
 @Service
@@ -61,6 +70,30 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
     @Autowired private RoleMenuService roleMenuService;
     @Autowired private SysMenuService sysMenuService;
     @Autowired private TenantService tenantService;
+    @Autowired private UserRoleMapper userRoleMapper;
+
+    @Override
+    public UserVO getUserById(Integer id) {
+        UserWithRolesDTO userWithRolesDTO = userMapper.selectUserWithRolesById(id);
+        if (Objects.nonNull(userWithRolesDTO)) {
+            return toVo(userWithRolesDTO);
+        }
+        return null;
+    }
+
+    @Override
+    public List<UserVO> listUsers(IPage<User> page, User user) {
+        return userMapper.listUsers(page, user).stream()
+                .map(this::toVo)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean checkUserNameUnique(User user) {
+        int userId = user.getId() == null ? -1 : user.getId();
+        User info = this.lambdaQuery().eq(User::getUsername, user.getUsername()).one();
+        return info == null || info.getId() == userId;
+    }
 
     /**
      * login by username and password.
@@ -189,4 +222,67 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
     public List<User> selectUnallocatedList(User user) {
         return userMapper.selectUnallocatedList(user);
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int insertUser(User user) {
+        this.save(user);
+        return insertUserRole(user);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int updateUser(User user) {
+        this.updateById(user);
+        userRoleMapper.deleteUserRoleByUserId(user.getId());
+        return insertUserRole(user);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int deleteUserByIds(Integer[] userIds) {
+        userRoleMapper.deleteUserRole(userIds);
+        return userMapper.deleteBatchIds(Arrays.asList(userIds));
+    }
+
+    private int insertUserRole(User user) {
+        int rows = 1;
+        if (user.getRoleIds() != null && user.getRoleIds().length > 0) {
+            List<UserRole> list = new ArrayList<>();
+            for (Integer roleId : user.getRoleIds()) {
+                UserRole userRole = new UserRole();
+                userRole.setUserId(user.getId());
+                userRole.setRoleId(roleId);
+                list.add(userRole);
+            }
+            if (!list.isEmpty()) {
+                rows = userRoleMapper.batchUserRole(list);
+            }
+        }
+        return rows;
+    }
+
+    private UserVO toVo(UserWithRolesDTO userWithRolesDTO) {
+        return UserVO.builder()
+                .id(userWithRolesDTO.getId())
+                .username(userWithRolesDTO.getUsername())
+                .nickname(
+                        StringUtils.isNotEmpty(userWithRolesDTO.getNickname())
+                                ? userWithRolesDTO.getNickname()
+                                : "")
+                .userType(userWithRolesDTO.getUserType() == 0 ? "LOCAL" : "LDAP")
+                .mobile(
+                        StringUtils.isNotEmpty(userWithRolesDTO.getMobile())
+                                ? userWithRolesDTO.getMobile()
+                                : "")
+                .email(
+                        StringUtils.isNotEmpty(userWithRolesDTO.getEmail())
+                                ? userWithRolesDTO.getEmail()
+                                : "")
+                .enabled(userWithRolesDTO.getEnabled())
+                .createTime(userWithRolesDTO.getCreateTime())
+                .updateTime(userWithRolesDTO.getUpdateTime())
+                .roles(userWithRolesDTO.getRoles())
+                .build();
+    }
 }
diff --git a/paimon-web-server/src/main/resources/i18n/messages.properties b/paimon-web-server/src/main/resources/i18n/messages.properties
index 8e74fbc..35f3a68 100644
--- a/paimon-web-server/src/main/resources/i18n/messages.properties
+++ b/paimon-web-server/src/main/resources/i18n/messages.properties
@@ -27,6 +27,7 @@ user.not.exist=User Not Exist
 user.password.error=User Password Error
 user.is.disabled=User Is Disabled
 user.not.bing.tenant=User Not Bing Tenant
+user.name.exist=The username {0} already exists.
 role.in.used=This role {0} is in used
 role.name.exist=This role name {0} exists.
 role.key.exist=This role key {0} exists.
diff --git a/paimon-web-server/src/main/resources/i18n/messages_en_US.properties b/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
index 8e74fbc..35f3a68 100644
--- a/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
+++ b/paimon-web-server/src/main/resources/i18n/messages_en_US.properties
@@ -27,6 +27,7 @@ user.not.exist=User Not Exist
 user.password.error=User Password Error
 user.is.disabled=User Is Disabled
 user.not.bing.tenant=User Not Bing Tenant
+user.name.exist=The username {0} already exists.
 role.in.used=This role {0} is in used
 role.name.exist=This role name {0} exists.
 role.key.exist=This role key {0} exists.
diff --git a/paimon-web-server/src/main/resources/i18n/messages_zh_CN.properties b/paimon-web-server/src/main/resources/i18n/messages_zh_CN.properties
index 8cffa97..96ca9fa 100644
--- a/paimon-web-server/src/main/resources/i18n/messages_zh_CN.properties
+++ b/paimon-web-server/src/main/resources/i18n/messages_zh_CN.properties
@@ -27,6 +27,7 @@ user.not.exist=\u7528\u6237\u4E0D\u5B58\u5728
 user.password.error=\u5BC6\u7801\u9519\u8BEF
 user.is.disabled=\u7528\u6237\u5DF2\u7981\u7528
 user.not.bing.tenant=\u7528\u6237\u672A\u7ED1\u5B9A\u79DF\u6237
+user.name.exist=\u8BE5\u7528\u6237\u540D{0}\u5DF2\u7ECF\u5B58\u5728
 role.in.used=\u6B64\u89D2\u8272{0}\u6B63\u5728\u4F7F\u7528\u4E2D
 role.name.exist=\u6B64\u89D2\u8272\u540D\u79F0{0}\u5DF2\u4F7F\u7528
 role.key.exist=\u6B64\u89D2\u8272\u5173\u952E\u5B57{0}\u5DF2\u4F7F\u7528
diff --git a/paimon-web-server/src/main/resources/mapper/UserMapper.xml b/paimon-web-server/src/main/resources/mapper/UserMapper.xml
index fc13d9f..a71b9a2 100644
--- a/paimon-web-server/src/main/resources/mapper/UserMapper.xml
+++ b/paimon-web-server/src/main/resources/mapper/UserMapper.xml
@@ -22,10 +22,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="org.apache.paimon.web.server.mapper.UserMapper">
 
+	<resultMap id="UserWithRolesResult" type="org.apache.paimon.web.server.data.dto.UserWithRolesDTO">
+		<id     property="id"       column="id"      />
+		<result property="username"     column="username"    />
+		<result property="nickname"     column="nickname"    />
+		<result property="userType"     column="user_type"    />
+		<result property="email"        column="email"        />
+		<result property="mobile"  column="mobile"  />
+		<result property="url"          column="url"          />
+		<result property="enabled"     column="enabled"     />
+		<result property="password"     column="password"     />
+		<result property="isDelete"       column="is_delete"       />
+		<result property="createTime"   column="create_time"  />
+		<result property="updateTime"   column="update_time"  />
+		<collection property="roles" resultMap="RoleResult" />
+	</resultMap>
+
     <resultMap type="org.apache.paimon.web.server.data.model.User" id="SysUserResult">
         <id     property="id"       column="id"      />
-        <result property="username"     column="user_name"    />
-        <result property="nickname"     column="nick_name"    />
+        <result property="username"     column="username"    />
+        <result property="nickname"     column="nickname"    />
+        <result property="userType"     column="user_type"    />
         <result property="email"        column="email"        />
         <result property="mobile"  column="mobile"  />
         <result property="url"          column="url"          />
@@ -34,11 +51,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="isDelete"       column="is_delete"       />
         <result property="createTime"   column="create_time"  />
         <result property="updateTime"   column="update_time"  />
-        <collection  property="roles"   javaType="java.util.List"           resultMap="RoleResult" />
     </resultMap>
 	
     <resultMap id="RoleResult" type="org.apache.paimon.web.server.data.model.SysRole">
-        <id     property="id"       column="role_id"        />
+        <id     property="id"       column="id"        />
         <result property="roleName"     column="role_name"      />
         <result property="roleKey"      column="role_key"       />
         <result property="sort"     column="sort"      />
@@ -46,33 +62,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 	
 	<sql id="selectUserVo">
-        select u.id, u.username, u.nickname, u.email, u.url, u.mobile, u.password, u.is_delete, u.enabled, u.create_time,
+        select u.id, u.username, u.nickname, u.user_type, u.email, u.url, u.mobile, u.password, u.is_delete, u.enabled, u.create_time,
         r.id, r.role_name, r.role_key, r.sort, r.enabled as role_status
         from user u
 		    left join user_role urole on u.id = urole.user_id
 		    left join sys_role r on r.id = urole.role_id
     </sql>
     
-    <select id="selectUserList" parameterType="org.apache.paimon.web.server.data.model.User" resultMap="SysUserResult">
-		select u.id, u.nickname, u.username, u.email, u.url, u.mobile, u.enabled, u.is_delete, u.create_time from user u
+    <select id="listUsers" parameterType="org.apache.paimon.web.server.data.model.User" resultMap="UserWithRolesResult">
+		<include refid="selectUserVo"/>
 		where u.is_delete = '0'
-		<if test="id != null and id != 0">
-			AND u.id = #{userId}
+		<if test="user.id != null and user.id != 0">
+			AND u.id = #{user.id}
 		</if>
-		<if test="username != null and username != ''">
-			AND u.username like concat('%', #{userName}, '%')
+		<if test="user.username != null and user.username != ''">
+			AND u.username like concat('%', #{user.username}, '%')
 		</if>
-		<if test="enabled != null and enabled != ''">
-			AND u.enabled = #{enabled}
+		<if test="user.enabled != null and user.enabled != ''">
+			AND u.enabled = #{user.enabled}
 		</if>
-		<if test="mobile != null and mobile != ''">
-			AND u.mobile like concat('%', #{mobile}, '%')
+		<if test="user.mobile != null and user.mobile != ''">
+			AND u.mobile like concat('%', #{user.mobile}, '%')
 		</if>
-		<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
-			AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
+		<if test="user.params.beginTime != null and user.params.beginTime != ''"><!-- Start time search -->
+			AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{user.params.beginTime},'%y%m%d')
 		</if>
-		<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
-			AND date_format(u.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
+		<if test="user.params.endTime != null and user.params.endTime != ''"><!-- End time search -->
+			AND date_format(u.create_time,'%y%m%d') &lt;= date_format(#{user.params.endTime},'%y%m%d')
 		</if>
 	</select>
 	
@@ -109,8 +125,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 	    <include refid="selectUserVo"/>
 		where u.username = #{username} and u.is_delete = '0'
 	</select>
-	
-	<select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
+
+	<select id="selectUserWithRolesById" parameterType="Integer" resultMap="UserWithRolesResult">
 		<include refid="selectUserVo"/>
 		where u.id = #{id}
 	</select>
diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/PermissionTest.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/PermissionTest.java
index 58a6596..55c5e96 100644
--- a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/PermissionTest.java
+++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/PermissionTest.java
@@ -18,9 +18,9 @@
 
 package org.apache.paimon.web.server.controller;
 
-import org.apache.paimon.web.server.data.model.User;
 import org.apache.paimon.web.server.data.result.R;
 import org.apache.paimon.web.server.data.result.enums.Status;
+import org.apache.paimon.web.server.data.vo.UserVO;
 import org.apache.paimon.web.server.util.ObjectMapperUtils;
 
 import com.fasterxml.jackson.core.type.TypeReference;
@@ -54,7 +54,7 @@ public class PermissionTest extends ControllerTestBase {
                         .getResponse()
                         .getContentAsString();
 
-        R<User> r = ObjectMapperUtils.fromJSON(responseString, new TypeReference<R<User>>() {});
+        R<UserVO> r = ObjectMapperUtils.fromJSON(responseString, new TypeReference<R<UserVO>>() {});
         assertEquals(Status.SUCCESS.getCode(), r.getCode());
     }
 }
diff --git a/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/UserControllerTest.java b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/UserControllerTest.java
new file mode 100644
index 0000000..eebeb95
--- /dev/null
+++ b/paimon-web-server/src/test/java/org/apache/paimon/web/server/controller/UserControllerTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.web.server.controller;
+
+import org.apache.paimon.web.server.data.model.User;
+import org.apache.paimon.web.server.data.result.PageR;
+import org.apache.paimon.web.server.data.result.R;
+import org.apache.paimon.web.server.data.vo.UserVO;
+import org.apache.paimon.web.server.util.ObjectMapperUtils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Test for {@link UserController}. */
+@SpringBootTest
+@AutoConfigureMockMvc
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class UserControllerTest extends ControllerTestBase {
+
+    private static final String userPath = "/api/user";
+
+    private static final int userId = 3;
+    private static final String username = "test";
+
+    @Test
+    @Order(1)
+    public void testAddUser() throws Exception {
+        User user = new User();
+        user.setId(userId);
+        user.setUsername(username);
+        user.setNickname(username);
+        user.setUserType(0);
+        user.setEnabled(true);
+        user.setIsDelete(false);
+
+        mockMvc.perform(
+                        MockMvcRequestBuilders.post(userPath)
+                                .cookie(cookie)
+                                .content(ObjectMapperUtils.toJSON(user))
+                                .contentType(MediaType.APPLICATION_JSON_VALUE)
+                                .accept(MediaType.APPLICATION_JSON_VALUE))
+                .andExpect(MockMvcResultMatchers.status().isOk())
+                .andDo(MockMvcResultHandlers.print());
+    }
+
+    @Test
+    @Order(2)
+    public void testGetUser() throws Exception {
+        String responseString =
+                mockMvc.perform(
+                                MockMvcRequestBuilders.get(userPath + "/" + userId)
+                                        .cookie(cookie)
+                                        .contentType(MediaType.APPLICATION_JSON_VALUE)
+                                        .accept(MediaType.APPLICATION_JSON_VALUE))
+                        .andExpect(MockMvcResultMatchers.status().isOk())
+                        .andDo(MockMvcResultHandlers.print())
+                        .andReturn()
+                        .getResponse()
+                        .getContentAsString();
+
+        R<UserVO> r = ObjectMapperUtils.fromJSON(responseString, new TypeReference<R<UserVO>>() {});
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData());
+        assertEquals(r.getData().getUsername(), username);
+    }
+
+    @Test
+    @Order(3)
+    public void testUpdateUser() throws Exception {
+        String newUserName = username + "-edit";
+        User user = new User();
+        user.setId(userId);
+        user.setUsername(newUserName);
+        user.setNickname(newUserName);
+        user.setUserType(0);
+        user.setEnabled(true);
+        user.setIsDelete(false);
+
+        mockMvc.perform(
+                        MockMvcRequestBuilders.put(userPath)
+                                .cookie(cookie)
+                                .content(ObjectMapperUtils.toJSON(user))
+                                .contentType(MediaType.APPLICATION_JSON_VALUE)
+                                .accept(MediaType.APPLICATION_JSON_VALUE))
+                .andExpect(MockMvcResultMatchers.status().isOk());
+
+        String responseString =
+                mockMvc.perform(
+                                MockMvcRequestBuilders.get(userPath + "/" + userId)
+                                        .cookie(cookie)
+                                        .contentType(MediaType.APPLICATION_JSON_VALUE)
+                                        .accept(MediaType.APPLICATION_JSON_VALUE))
+                        .andExpect(MockMvcResultMatchers.status().isOk())
+                        .andDo(MockMvcResultHandlers.print())
+                        .andReturn()
+                        .getResponse()
+                        .getContentAsString();
+
+        R<UserVO> r = ObjectMapperUtils.fromJSON(responseString, new TypeReference<R<UserVO>>() {});
+        assertEquals(200, r.getCode());
+        assertNotNull(r.getData());
+        assertEquals(r.getData().getUsername(), newUserName);
+    }
+
+    @Test
+    @Order(4)
+    public void testListUsers() throws Exception {
+        String responseString =
+                mockMvc.perform(
+                                MockMvcRequestBuilders.get(userPath + "/list")
+                                        .cookie(cookie)
+                                        .contentType(MediaType.APPLICATION_JSON_VALUE)
+                                        .accept(MediaType.APPLICATION_JSON_VALUE))
+                        .andExpect(MockMvcResultMatchers.status().isOk())
+                        .andDo(MockMvcResultHandlers.print())
+                        .andReturn()
+                        .getResponse()
+                        .getContentAsString();
+
+        PageR<UserVO> r =
+                ObjectMapperUtils.fromJSON(responseString, new TypeReference<PageR<UserVO>>() {});
+        assertTrue(
+                r.getData() != null
+                        && ((r.getTotal() > 0 && r.getData().size() > 0)
+                                || (r.getTotal() == 0 && r.getData().size() == 0)));
+
+        UserVO firstUser = r.getData().get(0);
+        assertEquals("admin", firstUser.getUsername());
+        assertEquals("Admin", firstUser.getNickname());
+        assertEquals("admin@paimon.com", firstUser.getEmail());
+        assertEquals("LOCAL", firstUser.getUserType());
+        assertTrue(firstUser.getEnabled());
+
+        UserVO secondUser = r.getData().get(1);
+        assertEquals("common", secondUser.getUsername());
+        assertEquals("common", secondUser.getNickname());
+        assertEquals("common@paimon.com", secondUser.getEmail());
+        assertEquals("LOCAL", secondUser.getUserType());
+        assertTrue(secondUser.getEnabled());
+    }
+
+    @Test
+    @Order(5)
+    public void testDeleteUser() throws Exception {
+        String delResponseString =
+                mockMvc.perform(
+                                MockMvcRequestBuilders.delete(
+                                                userPath + "/" + userId + "," + userId)
+                                        .cookie(cookie)
+                                        .contentType(MediaType.APPLICATION_JSON_VALUE)
+                                        .accept(MediaType.APPLICATION_JSON_VALUE))
+                        .andExpect(MockMvcResultMatchers.status().isOk())
+                        .andDo(MockMvcResultHandlers.print())
+                        .andReturn()
+                        .getResponse()
+                        .getContentAsString();
+
+        R<?> result = ObjectMapperUtils.fromJSON(delResponseString, R.class);
+        assertEquals(200, result.getCode());
+    }
+}