feat: support user manager #860

This commit is contained in:
tjq 2024-03-16 18:41:33 +08:00
parent 5e7751f092
commit 9b5916daf3
13 changed files with 238 additions and 27 deletions

View File

@ -16,6 +16,10 @@ public enum AuthErrorCode {
USER_NOT_LOGIN("-100", "UserNotLoggedIn"), USER_NOT_LOGIN("-100", "UserNotLoggedIn"),
USER_NOT_EXIST("-101", "UserNotExist"), USER_NOT_EXIST("-101", "UserNotExist"),
USER_AUTH_FAILED("-102", "UserAuthFailed"), USER_AUTH_FAILED("-102", "UserAuthFailed"),
/**
* 账户被停用
*/
USER_DISABLED("-103", "UserDisabled"),
NO_PERMISSION("-200", "NoPermission"), NO_PERMISSION("-200", "NoPermission"),

View File

@ -22,6 +22,7 @@ import tech.powerjob.server.auth.login.*;
import tech.powerjob.server.auth.service.login.LoginRequest; import tech.powerjob.server.auth.service.login.LoginRequest;
import tech.powerjob.server.auth.service.login.PowerJobLoginService; import tech.powerjob.server.auth.service.login.PowerJobLoginService;
import tech.powerjob.server.common.Loggers; import tech.powerjob.server.common.Loggers;
import tech.powerjob.server.common.constants.SwitchableStatus;
import tech.powerjob.server.persistence.remote.model.UserInfoDO; import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
@ -108,9 +109,11 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService {
powerJobUserOpt = userInfoRepository.findByUsername(dbUserName); powerJobUserOpt = userInfoRepository.findByUsername(dbUserName);
} else { } else {
// 更新二次校验的 TOKEN 信息
UserInfoDO dbUserInfoDO = powerJobUserOpt.get(); UserInfoDO dbUserInfoDO = powerJobUserOpt.get();
checkUserStatus(dbUserInfoDO);
// 更新二次校验的 TOKEN 信息
dbUserInfoDO.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo())); dbUserInfoDO.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo()));
dbUserInfoDO.setGmtModified(new Date()); dbUserInfoDO.setGmtModified(new Date());
@ -147,6 +150,8 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService {
UserInfoDO dbUser = dbUserInfoOpt.get(); UserInfoDO dbUser = dbUserInfoOpt.get();
checkUserStatus(dbUser);
PowerJobUser powerJobUser = new PowerJobUser(); PowerJobUser powerJobUser = new PowerJobUser();
String tokenLoginVerifyInfoStr = dbUser.getTokenLoginVerifyInfo(); String tokenLoginVerifyInfoStr = dbUser.getTokenLoginVerifyInfo();
@ -174,6 +179,17 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService {
return Optional.of(powerJobUser); return Optional.of(powerJobUser);
} }
/**
* 检查 user 状态
* @param dbUser user
*/
private void checkUserStatus(UserInfoDO dbUser) {
int accountStatus = Optional.ofNullable(dbUser.getStatus()).orElse(SwitchableStatus.ENABLE.getV());
if (accountStatus == SwitchableStatus.DISABLE.getV()) {
throw new PowerJobAuthException(AuthErrorCode.USER_DISABLED);
}
}
private ThirdPartyLoginService fetchBizLoginService(String loginType) { private ThirdPartyLoginService fetchBizLoginService(String loginType) {
final ThirdPartyLoginService loginService = code2ThirdPartyLoginService.get(loginType); final ThirdPartyLoginService loginService = code2ThirdPartyLoginService.get(loginType);
if (loginService == null) { if (loginService == null) {

View File

@ -71,6 +71,11 @@ public class UserInfoDO {
*/ */
private String originUsername; private String originUsername;
/**
* 账号当前状态
*/
private Integer status;
private Date gmtCreate; private Date gmtCreate;
private Date gmtModified; private Date gmtModified;

View File

@ -1,7 +1,8 @@
package tech.powerjob.server.persistence.remote.repository; package tech.powerjob.server.persistence.remote.repository;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -12,7 +13,7 @@ import java.util.Optional;
* @author tjq * @author tjq
* @since 2020/4/12 * @since 2020/4/12
*/ */
public interface UserInfoRepository extends JpaRepository<UserInfoDO, Long> { public interface UserInfoRepository extends JpaRepository<UserInfoDO, Long>, JpaSpecificationExecutor<UserInfoDO> {
Optional<UserInfoDO> findByUsername(String username); Optional<UserInfoDO> findByUsername(String username);

View File

@ -52,5 +52,11 @@ public interface WebAuthService {
*/ */
boolean hasPermission(RoleScope roleScope, Long target, Permission permission); boolean hasPermission(RoleScope roleScope, Long target, Permission permission);
/**
* 是否为全局管理员
* @return true or false
*/
boolean isGlobalAdmin();
Map<Role, List<Long>> fetchMyPermissionTargets(RoleScope roleScope); Map<Role, List<Long>> fetchMyPermissionTargets(RoleScope roleScope);
} }

View File

@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.server.auth.*; import tech.powerjob.server.auth.*;
import tech.powerjob.server.auth.common.AuthConstants;
import tech.powerjob.server.auth.common.AuthErrorCode; import tech.powerjob.server.auth.common.AuthErrorCode;
import tech.powerjob.server.auth.common.PowerJobAuthException; import tech.powerjob.server.auth.common.PowerJobAuthException;
import tech.powerjob.server.auth.service.WebAuthService; import tech.powerjob.server.auth.service.WebAuthService;
@ -71,6 +72,11 @@ public class WebAuthServiceImpl implements WebAuthService {
return powerJobPermissionService.hasPermission(powerJobUser.getId(), roleScope, target, permission); return powerJobPermissionService.hasPermission(powerJobUser.getId(), roleScope, target, permission);
} }
@Override
public boolean isGlobalAdmin() {
return hasPermission(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID, Permission.SU);
}
@Override @Override
public Map<Role, List<Long>> fetchMyPermissionTargets(RoleScope roleScope) { public Map<Role, List<Long>> fetchMyPermissionTargets(RoleScope roleScope) {

View File

@ -4,18 +4,23 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import tech.powerjob.common.exception.PowerJobException;
import tech.powerjob.common.response.ResultDTO; import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.PowerJobUser; import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.Role; import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope; import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.common.AuthErrorCode; import tech.powerjob.server.auth.common.AuthErrorCode;
import tech.powerjob.server.auth.common.PowerJobAuthException; import tech.powerjob.server.auth.common.PowerJobAuthException;
import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.auth.service.WebAuthService; import tech.powerjob.server.auth.service.WebAuthService;
import tech.powerjob.server.auth.service.login.PowerJobLoginService; import tech.powerjob.server.auth.service.login.PowerJobLoginService;
import tech.powerjob.server.common.constants.SwitchableStatus;
import tech.powerjob.server.persistence.remote.model.AppInfoDO; import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import tech.powerjob.server.persistence.remote.model.NamespaceDO; import tech.powerjob.server.persistence.remote.model.NamespaceDO;
import tech.powerjob.server.persistence.remote.model.UserInfoDO; import tech.powerjob.server.persistence.remote.model.UserInfoDO;
@ -25,10 +30,12 @@ import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import tech.powerjob.server.web.converter.NamespaceConverter; import tech.powerjob.server.web.converter.NamespaceConverter;
import tech.powerjob.server.web.converter.UserConverter; import tech.powerjob.server.web.converter.UserConverter;
import tech.powerjob.server.web.request.ModifyUserInfoRequest; import tech.powerjob.server.web.request.ModifyUserInfoRequest;
import tech.powerjob.server.web.request.QueryUserRequest;
import tech.powerjob.server.web.response.AppBaseVO; import tech.powerjob.server.web.response.AppBaseVO;
import tech.powerjob.server.web.response.NamespaceBaseVO; import tech.powerjob.server.web.response.NamespaceBaseVO;
import tech.powerjob.server.web.response.UserBaseVO; import tech.powerjob.server.web.response.UserBaseVO;
import tech.powerjob.server.web.response.UserDetailVO; import tech.powerjob.server.web.response.UserDetailVO;
import tech.powerjob.server.web.service.UserWebService;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -41,9 +48,14 @@ import java.util.stream.Collectors;
* @author tjq * @author tjq
* @since 2020/4/12 * @since 2020/4/12
*/ */
@Slf4j
@RestController @RestController
@RequestMapping("/user") @RequestMapping("/user")
public class UserInfoController { public class UserInfoController {
@Resource
private UserWebService userWebService;
@Resource @Resource
private UserInfoRepository userInfoRepository; private UserInfoRepository userInfoRepository;
@Resource @Resource
@ -59,21 +71,14 @@ public class UserInfoController {
@PostMapping("/modify") @PostMapping("/modify")
public ResultDTO<Void> modifyUser(@RequestBody ModifyUserInfoRequest modifyUserInfoRequest, HttpServletRequest httpServletRequest) { public ResultDTO<Void> modifyUser(@RequestBody ModifyUserInfoRequest modifyUserInfoRequest, HttpServletRequest httpServletRequest) {
Optional<PowerJobUser> powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest);
if (!powerJobUserOpt.isPresent()) {
throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN);
}
Long userId = modifyUserInfoRequest.getId(); Long userId = modifyUserInfoRequest.getId();
checkModifyUserPermission(userId, httpServletRequest);
Optional<UserInfoDO> userOpt = userInfoRepository.findById(userId); Optional<UserInfoDO> userOpt = userInfoRepository.findById(userId);
if (!userOpt.isPresent()) { if (!userOpt.isPresent()) {
throw new IllegalArgumentException("can't find user by userId:" + userId); throw new IllegalArgumentException("can't find user by userId:" + userId);
} }
if (!Objects.equals(powerJobUserOpt.get().getId(), userId)) {
throw new IllegalAccessException("no permission to change others user info");
}
UserInfoDO dbUser = userOpt.get(); UserInfoDO dbUser = userOpt.get();
// 拷入允许修改的内容 // 拷入允许修改的内容
@ -111,6 +116,19 @@ public class UserInfoController {
return ResultDTO.success(convert(result)); return ResultDTO.success(convert(result));
} }
/**
* 查询用户信息用于管理员操作会返回敏感信息
* @param queryUserRequest 查询请求
* @return 响应
*/
@PostMapping("/query")
@ApiPermission(name = "User-Query", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.SU)
public ResultDTO<List<UserBaseVO>> query(@RequestBody QueryUserRequest queryUserRequest) {
List<UserInfoDO> userInfoDos = userWebService.list(queryUserRequest);
List<UserBaseVO> userBaseVOS = userInfoDos.stream().map(x -> UserConverter.do2BaseVo(x, true)).collect(Collectors.toList());
return ResultDTO.success(userBaseVOS);
}
@GetMapping("/detail") @GetMapping("/detail")
public ResultDTO<UserDetailVO> getUserDetail(HttpServletRequest httpServletRequest) { public ResultDTO<UserDetailVO> getUserDetail(HttpServletRequest httpServletRequest) {
Optional<PowerJobUser> powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest); Optional<PowerJobUser> powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest);
@ -171,11 +189,62 @@ public class UserInfoController {
return ResultDTO.success(userDetailVO); return ResultDTO.success(userDetailVO);
} }
@PostMapping("/disable")
public ResultDTO<Void> disableUser(Long uid, HttpServletRequest httpServletRequest) {
changeAccountStatus(uid, SwitchableStatus.DISABLE, httpServletRequest);
return ResultDTO.success(null);
}
@PostMapping("/enable")
public ResultDTO<Void> enableUser(Long uid, HttpServletRequest httpServletRequest) {
changeAccountStatus(uid, SwitchableStatus.ENABLE, httpServletRequest);
return ResultDTO.success(null);
}
private void changeAccountStatus(Long uid, SwitchableStatus targetStatus, HttpServletRequest httpServletRequest) {
checkModifyUserPermission(uid, httpServletRequest);
Optional<UserInfoDO> userOpt = userInfoRepository.findById(uid);
if (!userOpt.isPresent()) {
throw new IllegalArgumentException("can't find user by userId:" + uid);
}
UserInfoDO dbUser = userOpt.get();
dbUser.setStatus(targetStatus.getV());
dbUser.setGmtModified(new Date());
userInfoRepository.saveAndFlush(dbUser);
log.info("[UserInfoController] changeAccountStatus, userId={},targetStatus={}", uid, targetStatus);
}
/**
* 检查针对 user 处理的权限
* @param uid 目标 userId
* @param httpServletRequest http 上下文请求
*/
private void checkModifyUserPermission(Long uid, HttpServletRequest httpServletRequest) {
Optional<PowerJobUser> powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest);
if (!powerJobUserOpt.isPresent()) {
throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN);
}
PowerJobUser currentLoginUser = powerJobUserOpt.get();
boolean myself = uid.equals(currentLoginUser.getId());
boolean globalAdmin = webAuthService.isGlobalAdmin();
if (myself || globalAdmin) {
return;
}
throw new PowerJobException("Only the administrator and account owner can modify the account");
}
private static List<UserBaseVO> convert(List<UserInfoDO> data) { private static List<UserBaseVO> convert(List<UserInfoDO> data) {
if (CollectionUtils.isEmpty(data)) { if (CollectionUtils.isEmpty(data)) {
return Lists.newLinkedList(); return Lists.newLinkedList();
} }
return data.stream().map(UserConverter::do2BaseVo).collect(Collectors.toList()); return data.stream().map(x -> UserConverter.do2BaseVo(x, false)).collect(Collectors.toList());
} }
private static Set<Long> mergeIds(Map<?, List<Long>> map) { private static Set<Long> mergeIds(Map<?, List<Long>> map) {

View File

@ -1,8 +1,11 @@
package tech.powerjob.server.web.converter; package tech.powerjob.server.web.converter;
import tech.powerjob.server.common.constants.SwitchableStatus;
import tech.powerjob.server.persistence.remote.model.UserInfoDO; import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.web.response.UserBaseVO; import tech.powerjob.server.web.response.UserBaseVO;
import java.util.Optional;
/** /**
* UserConverter * UserConverter
* *
@ -11,13 +14,21 @@ import tech.powerjob.server.web.response.UserBaseVO;
*/ */
public class UserConverter { public class UserConverter {
public static UserBaseVO do2BaseVo(UserInfoDO x) { public static UserBaseVO do2BaseVo(UserInfoDO x, boolean includeSensitiveInfo) {
UserBaseVO userBaseVO = new UserBaseVO(); UserBaseVO userBaseVO = new UserBaseVO();
userBaseVO.setId(x.getId()); userBaseVO.setId(x.getId());
userBaseVO.setAccountType(x.getAccountType());
userBaseVO.setUsername(x.getUsername()); userBaseVO.setUsername(x.getUsername());
userBaseVO.setNick(x.getNick()); userBaseVO.setNick(x.getNick());
userBaseVO.setStatus(Optional.ofNullable(x.getStatus()).orElse(SwitchableStatus.ENABLE.getV()));
userBaseVO.setEnable(userBaseVO.getStatus() == SwitchableStatus.ENABLE.getV());
if (includeSensitiveInfo) {
userBaseVO.setPhone(x.getPhone());
userBaseVO.setEmail(x.getEmail());
}
userBaseVO.genShowName(); userBaseVO.genShowName();
return userBaseVO; return userBaseVO;

View File

@ -0,0 +1,32 @@
package tech.powerjob.server.web.request;
import lombok.Data;
import java.io.Serializable;
/**
* 用户查询请求
*
* @author tjq
* @since 2024/3/16
*/
@Data
public class QueryUserRequest implements Serializable {
/**
* 通过 userId 精确查询
*/
private Long userIdEq;
private String accountTypeEq;
/**
* nick 模糊查询
*/
private String nickLike;
/**
* 手机号模糊查询
*/
private String phoneLike;
}

View File

@ -15,10 +15,32 @@ import org.apache.commons.lang3.StringUtils;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
public class UserBaseVO { public class UserBaseVO {
protected Long id; protected Long id;
protected String username; protected String username;
protected String nick; protected String nick;
/**
* 账户类型
*/
private String accountType;
/**
* 手机号
*/
private String phone;
/**
* 邮箱地址
*/
private String email;
/**
* 账号当前状态
*/
private Integer status;
private boolean enable;
/** /**
* 前端展示名称更容易辨认 * 前端展示名称更容易辨认
*/ */

View File

@ -18,23 +18,12 @@ import java.util.Map;
@ToString @ToString
public class UserDetailVO extends UserBaseVO { public class UserDetailVO extends UserBaseVO {
/**
* 账户类型
*/
private String accountType;
/** /**
* 密码 * 密码
*/ */
private String password; private String password;
/**
* 手机号
*/
private String phone;
/**
* 邮箱地址
*/
private String email;
/** /**
* webHook * webHook
*/ */

View File

@ -1,7 +1,10 @@
package tech.powerjob.server.web.service; package tech.powerjob.server.web.service;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.web.request.QueryUserRequest;
import tech.powerjob.server.web.response.UserBaseVO; import tech.powerjob.server.web.response.UserBaseVO;
import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
@ -13,4 +16,6 @@ import java.util.Optional;
public interface UserWebService { public interface UserWebService {
Optional<UserBaseVO> fetchBaseUserInfo(Long userId); Optional<UserBaseVO> fetchBaseUserInfo(Long userId);
List<UserInfoDO> list(QueryUserRequest queryUserRequest);
} }

View File

@ -2,14 +2,21 @@ package tech.powerjob.server.web.service.impl;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import tech.powerjob.server.persistence.QueryConvertUtils;
import tech.powerjob.server.persistence.remote.model.UserInfoDO; import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import tech.powerjob.server.web.converter.UserConverter; import tech.powerjob.server.web.converter.UserConverter;
import tech.powerjob.server.web.request.QueryUserRequest;
import tech.powerjob.server.web.response.UserBaseVO; import tech.powerjob.server.web.response.UserBaseVO;
import tech.powerjob.server.web.service.UserWebService; import tech.powerjob.server.web.service.UserWebService;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.persistence.criteria.Predicate;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -50,9 +57,47 @@ public class UserWebServiceImpl implements UserWebService {
throw new IllegalArgumentException("can't find user by userId: " + userId); throw new IllegalArgumentException("can't find user by userId: " + userId);
}); });
return Optional.of(UserConverter.do2BaseVo(userInfoDO)); return Optional.of(UserConverter.do2BaseVo(userInfoDO, false));
} catch (Exception e) { } catch (Exception e) {
return Optional.empty(); return Optional.empty();
} }
} }
@Override
public List<UserInfoDO> list(QueryUserRequest q) {
Long userIdEq = q.getUserIdEq();
String accountTypeEq = q.getAccountTypeEq();
String nickLike = q.getNickLike();
String phoneLike = q.getPhoneLike();
Specification<UserInfoDO> specification = (root, query, cb) -> {
List<Predicate> predicates = Lists.newArrayList();
if (userIdEq != null) {
predicates.add(cb.equal(root.get("id"), userIdEq));
}
if (StringUtils.isNotEmpty(accountTypeEq)) {
predicates.add(cb.equal(root.get("accountType"), accountTypeEq));
}
if (StringUtils.isNotEmpty(nickLike)) {
predicates.add(cb.like(root.get("nick"), QueryConvertUtils.convertLikeParams(nickLike)));
}
if (StringUtils.isNotEmpty(phoneLike)) {
predicates.add(cb.like(root.get("phone"), QueryConvertUtils.convertLikeParams(phoneLike)));
}
if (predicates.isEmpty()) {
return null;
}
return query.where(predicates.toArray(new Predicate[0])).getRestriction();
};
return userInfoRepository.findAll(specification);
}
} }