|
|
@@ -1,14 +1,23 @@
|
|
|
package com.wenshu.platform.service.auth;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.util.List;
|
|
|
+import java.util.UUID;
|
|
|
|
|
|
+import com.wenshu.platform.dao.OrganizationDAO;
|
|
|
import com.wenshu.platform.dao.UserDAO;
|
|
|
import com.wenshu.platform.model.bo.LoginUserBO;
|
|
|
+import com.wenshu.platform.model.dataobject.OrganizationDO;
|
|
|
import com.wenshu.platform.model.dataobject.UserDO;
|
|
|
import com.wenshu.platform.model.enums.UserRole;
|
|
|
+import com.wenshu.platform.model.req.AdminUserCreateReq;
|
|
|
+import com.wenshu.platform.model.req.AdminUserResetPasswordReq;
|
|
|
+import com.wenshu.platform.model.req.AdminUserUpdateReq;
|
|
|
+import com.wenshu.platform.model.req.InviteCodeUpdateReq;
|
|
|
import com.wenshu.platform.model.req.UserLoginReq;
|
|
|
import com.wenshu.platform.model.req.UserRegisterReq;
|
|
|
import com.wenshu.platform.model.req.UserUpdateReq;
|
|
|
+import com.wenshu.platform.model.resp.InviteCodeResp;
|
|
|
import com.wenshu.platform.model.resp.UserLoginResp;
|
|
|
import com.wenshu.platform.model.resp.UserProfileResp;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
@@ -25,9 +34,12 @@ public class AuthService {
|
|
|
private static final int EMAIL_MAX_LENGTH = 128;
|
|
|
private static final int PASSWORD_MIN_LENGTH = 6;
|
|
|
private static final int PASSWORD_MAX_LENGTH = 64;
|
|
|
+ private static final int INVITE_CODE_MAX_LENGTH = 64;
|
|
|
private static final int USER_ENABLED = 1;
|
|
|
+ private static final long INVITE_CONFIG_ID = 1L;
|
|
|
|
|
|
private final UserDAO userDAO;
|
|
|
+ private final OrganizationDAO organizationDAO;
|
|
|
private final AuthTokenService authTokenService;
|
|
|
private final PasswordEncoder passwordEncoder;
|
|
|
|
|
|
@@ -35,35 +47,20 @@ public class AuthService {
|
|
|
if (req == null) {
|
|
|
throw new IllegalArgumentException("request body cannot be null");
|
|
|
}
|
|
|
-
|
|
|
- String username = normalizeUsername(req.getUsername());
|
|
|
- String password = req.getPassword();
|
|
|
- String email = normalizeText(req.getEmail());
|
|
|
- UserRole role = validateRegisterInput(username, password, email, req.getRole());
|
|
|
-
|
|
|
- UserDO existingUser = userDAO.findByUsername(username);
|
|
|
- if (existingUser != null) {
|
|
|
- throw new IllegalArgumentException("username already exists");
|
|
|
+ String inviteCode = normalizeText(req.getInviteCode());
|
|
|
+ if (!StringUtils.hasText(inviteCode)) {
|
|
|
+ throw new IllegalArgumentException("inviteCode cannot be empty");
|
|
|
}
|
|
|
-
|
|
|
- UserDO newUser = new UserDO();
|
|
|
- newUser.setUsername(username);
|
|
|
- newUser.setPasswordHash(passwordEncoder.encode(password));
|
|
|
- newUser.setRole(role.getCode());
|
|
|
- newUser.setEmail(email);
|
|
|
- newUser.setStatus(USER_ENABLED);
|
|
|
- newUser.setCreatedAt(LocalDateTime.now());
|
|
|
- userDAO.save(newUser);
|
|
|
-
|
|
|
- LoginUserBO loginUser = authTokenService.createSession(newUser);
|
|
|
- UserLoginResp resp = new UserLoginResp();
|
|
|
- resp.setToken(loginUser.getToken());
|
|
|
- resp.setExpiresAt(loginUser.getExpiresAt());
|
|
|
- resp.setUserId(loginUser.getUserId());
|
|
|
- resp.setUsername(loginUser.getUsername());
|
|
|
- resp.setRole(loginUser.getRole());
|
|
|
- resp.setEmail(loginUser.getEmail());
|
|
|
- return resp;
|
|
|
+ if (inviteCode.length() > INVITE_CODE_MAX_LENGTH) {
|
|
|
+ throw new IllegalArgumentException("inviteCode length exceeds " + INVITE_CODE_MAX_LENGTH);
|
|
|
+ }
|
|
|
+ OrganizationDO organization = organizationDAO.findByInviteCode(inviteCode);
|
|
|
+ if (organization == null) {
|
|
|
+ throw new IllegalArgumentException("invalid inviteCode");
|
|
|
+ }
|
|
|
+ UserRole role = resolveInviteRole(organization, inviteCode);
|
|
|
+ UserDO created = createUserInternal(req.getUsername(), req.getPassword(), req.getEmail(), role.getCode());
|
|
|
+ return buildLoginResp(authTokenService.createSession(created));
|
|
|
}
|
|
|
|
|
|
public UserLoginResp login(UserLoginReq req) {
|
|
|
@@ -86,26 +83,12 @@ public class AuthService {
|
|
|
if (!isPasswordValid(password, userDO.getPasswordHash())) {
|
|
|
throw new UnauthorizedException("Invalid username or password");
|
|
|
}
|
|
|
-
|
|
|
- LoginUserBO loginUser = authTokenService.createSession(userDO);
|
|
|
- UserLoginResp resp = new UserLoginResp();
|
|
|
- resp.setToken(loginUser.getToken());
|
|
|
- resp.setExpiresAt(loginUser.getExpiresAt());
|
|
|
- resp.setUserId(loginUser.getUserId());
|
|
|
- resp.setUsername(loginUser.getUsername());
|
|
|
- resp.setRole(loginUser.getRole());
|
|
|
- resp.setEmail(loginUser.getEmail());
|
|
|
- return resp;
|
|
|
+ return buildLoginResp(authTokenService.createSession(userDO));
|
|
|
}
|
|
|
|
|
|
public UserProfileResp getCurrentUser(String authorization) {
|
|
|
LoginUserBO loginUser = requireLoginUser(authorization);
|
|
|
- UserProfileResp profileResp = new UserProfileResp();
|
|
|
- profileResp.setUserId(loginUser.getUserId());
|
|
|
- profileResp.setUsername(loginUser.getUsername());
|
|
|
- profileResp.setRole(loginUser.getRole());
|
|
|
- profileResp.setEmail(loginUser.getEmail());
|
|
|
- return profileResp;
|
|
|
+ return toUserProfile(loginUser, resolveUserStatus(loginUser.getUserId()));
|
|
|
}
|
|
|
|
|
|
public UserProfileResp updateCurrentUser(String authorization, UserUpdateReq req) {
|
|
|
@@ -125,10 +108,7 @@ public class AuthService {
|
|
|
throw new IllegalArgumentException("email length exceeds " + EMAIL_MAX_LENGTH);
|
|
|
}
|
|
|
if (StringUtils.hasText(newPassword)) {
|
|
|
- if (newPassword.length() < PASSWORD_MIN_LENGTH || newPassword.length() > PASSWORD_MAX_LENGTH) {
|
|
|
- throw new IllegalArgumentException(
|
|
|
- "password length must be between " + PASSWORD_MIN_LENGTH + " and " + PASSWORD_MAX_LENGTH);
|
|
|
- }
|
|
|
+ validatePassword(newPassword);
|
|
|
}
|
|
|
|
|
|
UserDO update = new UserDO();
|
|
|
@@ -149,6 +129,7 @@ public class AuthService {
|
|
|
resp.setUsername(effectiveUsername);
|
|
|
resp.setRole(loginUser.getRole());
|
|
|
resp.setEmail(effectiveEmail);
|
|
|
+ resp.setStatus(resolveUserStatus(loginUser.getUserId()));
|
|
|
return resp;
|
|
|
}
|
|
|
|
|
|
@@ -157,6 +138,139 @@ public class AuthService {
|
|
|
authTokenService.invalidate(token);
|
|
|
}
|
|
|
|
|
|
+ public List<UserProfileResp> listUsers(String authorization) {
|
|
|
+ requireAdminUser(authorization);
|
|
|
+ return userDAO.findAll().stream()
|
|
|
+ .map(this::toUserProfile)
|
|
|
+ .toList();
|
|
|
+ }
|
|
|
+
|
|
|
+ public UserProfileResp createUserByAdmin(String authorization, AdminUserCreateReq req) {
|
|
|
+ requireAdminUser(authorization);
|
|
|
+ if (req == null) {
|
|
|
+ throw new IllegalArgumentException("request body cannot be null");
|
|
|
+ }
|
|
|
+ UserDO created = createUserInternal(req.getUsername(), req.getPassword(), req.getEmail(), req.getRole());
|
|
|
+ return toUserProfile(created);
|
|
|
+ }
|
|
|
+
|
|
|
+ public UserProfileResp updateUserByAdmin(String authorization, Long userId, AdminUserUpdateReq req) {
|
|
|
+ LoginUserBO adminUser = requireAdminUser(authorization);
|
|
|
+ if (userId == null) {
|
|
|
+ throw new IllegalArgumentException("userId cannot be null");
|
|
|
+ }
|
|
|
+ if (req == null) {
|
|
|
+ throw new IllegalArgumentException("request body cannot be null");
|
|
|
+ }
|
|
|
+ UserDO target = requireUserExists(userId);
|
|
|
+
|
|
|
+ String normalizedEmail = normalizeText(req.getEmail());
|
|
|
+ if (normalizedEmail != null && normalizedEmail.length() > EMAIL_MAX_LENGTH) {
|
|
|
+ throw new IllegalArgumentException("email length exceeds " + EMAIL_MAX_LENGTH);
|
|
|
+ }
|
|
|
+
|
|
|
+ UserDO update = new UserDO();
|
|
|
+ update.setUserId(userId);
|
|
|
+ update.setEmail(normalizedEmail);
|
|
|
+ update.setStatus(normalizeStatus(req.getStatus()));
|
|
|
+ if (StringUtils.hasText(req.getRole())) {
|
|
|
+ UserRole role = UserRole.fromInput(req.getRole());
|
|
|
+ if (role == null) {
|
|
|
+ throw new IllegalArgumentException("role must be ADMIN, DEVELOPER or ANALYST");
|
|
|
+ }
|
|
|
+ update.setRole(role.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (userId.equals(adminUser.getUserId()) && update.getStatus() != null && update.getStatus() <= 0) {
|
|
|
+ throw new IllegalArgumentException("admin cannot disable self");
|
|
|
+ }
|
|
|
+ if (userId.equals(adminUser.getUserId()) && UserRole.ANALYST.getCode().equals(update.getRole())) {
|
|
|
+ throw new IllegalArgumentException("admin cannot change self role to ANALYST");
|
|
|
+ }
|
|
|
+
|
|
|
+ userDAO.updateAdminFieldsByUserId(update);
|
|
|
+
|
|
|
+ UserDO reloaded = userDAO.findByUserId(target.getUserId());
|
|
|
+ return toUserProfile(reloaded);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void resetPasswordByAdmin(String authorization, Long userId, AdminUserResetPasswordReq req) {
|
|
|
+ requireAdminUser(authorization);
|
|
|
+ if (userId == null) {
|
|
|
+ throw new IllegalArgumentException("userId cannot be null");
|
|
|
+ }
|
|
|
+ if (req == null) {
|
|
|
+ throw new IllegalArgumentException("request body cannot be null");
|
|
|
+ }
|
|
|
+ requireUserExists(userId);
|
|
|
+ validatePassword(req.getPassword());
|
|
|
+
|
|
|
+ UserDO update = new UserDO();
|
|
|
+ update.setUserId(userId);
|
|
|
+ update.setPasswordHash(passwordEncoder.encode(req.getPassword()));
|
|
|
+ userDAO.updateAdminFieldsByUserId(update);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void deleteUserByAdmin(String authorization, Long userId) {
|
|
|
+ LoginUserBO adminUser = requireAdminUser(authorization);
|
|
|
+ if (userId == null) {
|
|
|
+ throw new IllegalArgumentException("userId cannot be null");
|
|
|
+ }
|
|
|
+ if (userId.equals(adminUser.getUserId())) {
|
|
|
+ throw new IllegalArgumentException("admin cannot delete self");
|
|
|
+ }
|
|
|
+ requireUserExists(userId);
|
|
|
+ int deleted = userDAO.deleteByUserId(userId);
|
|
|
+ if (deleted <= 0) {
|
|
|
+ throw new IllegalStateException("User changed, please retry");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public InviteCodeResp getInviteCodes(String authorization) {
|
|
|
+ requireAdminUser(authorization);
|
|
|
+ OrganizationDO organization = organizationDAO.findById(INVITE_CONFIG_ID);
|
|
|
+ if (organization == null) {
|
|
|
+ throw new IllegalStateException("invite code config does not exist");
|
|
|
+ }
|
|
|
+ String analystCode = normalizeText(organization.getAnalystInviteCode());
|
|
|
+ String developerCode = normalizeText(organization.getDeveloperInviteCode());
|
|
|
+ if (!StringUtils.hasText(analystCode) || !StringUtils.hasText(developerCode)) {
|
|
|
+ analystCode = generateInviteCode("ANALYST");
|
|
|
+ developerCode = generateInviteCode("DEVELOPER");
|
|
|
+ while (analystCode.equals(developerCode)) {
|
|
|
+ developerCode = generateInviteCode("DEVELOPER");
|
|
|
+ }
|
|
|
+ organizationDAO.updateInviteCodes(INVITE_CONFIG_ID, analystCode, developerCode);
|
|
|
+ }
|
|
|
+ InviteCodeResp resp = new InviteCodeResp();
|
|
|
+ resp.setAnalystInviteCode(analystCode);
|
|
|
+ resp.setDeveloperInviteCode(developerCode);
|
|
|
+ return resp;
|
|
|
+ }
|
|
|
+
|
|
|
+ public InviteCodeResp updateInviteCodes(String authorization, InviteCodeUpdateReq req) {
|
|
|
+ requireAdminUser(authorization);
|
|
|
+ if (req == null) {
|
|
|
+ throw new IllegalArgumentException("request body cannot be null");
|
|
|
+ }
|
|
|
+ String analystCode = normalizeText(req.getAnalystInviteCode());
|
|
|
+ String developerCode = normalizeText(req.getDeveloperInviteCode());
|
|
|
+ if (!StringUtils.hasText(analystCode) || !StringUtils.hasText(developerCode)) {
|
|
|
+ throw new IllegalArgumentException("invite codes cannot be empty");
|
|
|
+ }
|
|
|
+ if (analystCode.length() > INVITE_CODE_MAX_LENGTH || developerCode.length() > INVITE_CODE_MAX_LENGTH) {
|
|
|
+ throw new IllegalArgumentException("invite code length exceeds " + INVITE_CODE_MAX_LENGTH);
|
|
|
+ }
|
|
|
+ if (analystCode.equals(developerCode)) {
|
|
|
+ throw new IllegalArgumentException("analyst and developer invite codes must be different");
|
|
|
+ }
|
|
|
+ organizationDAO.updateInviteCodes(INVITE_CONFIG_ID, analystCode, developerCode);
|
|
|
+ InviteCodeResp resp = new InviteCodeResp();
|
|
|
+ resp.setAnalystInviteCode(analystCode);
|
|
|
+ resp.setDeveloperInviteCode(developerCode);
|
|
|
+ return resp;
|
|
|
+ }
|
|
|
+
|
|
|
public LoginUserBO requireLoginUser(String authorization) {
|
|
|
String token = parseBearerToken(authorization);
|
|
|
LoginUserBO loginUser = authTokenService.resolveValidUser(token);
|
|
|
@@ -166,6 +280,15 @@ public class AuthService {
|
|
|
return loginUser;
|
|
|
}
|
|
|
|
|
|
+ public LoginUserBO requireAdminUser(String authorization) {
|
|
|
+ LoginUserBO loginUser = requireLoginUser(authorization);
|
|
|
+ UserRole role = UserRole.fromInput(loginUser.getRole());
|
|
|
+ if (role == null || !role.isAdmin()) {
|
|
|
+ throw new ForbiddenException("ADMIN role required");
|
|
|
+ }
|
|
|
+ return loginUser;
|
|
|
+ }
|
|
|
+
|
|
|
public String parseBearerToken(String authorization) {
|
|
|
if (!StringUtils.hasText(authorization)) {
|
|
|
return null;
|
|
|
@@ -181,6 +304,79 @@ public class AuthService {
|
|
|
return StringUtils.hasText(token) ? token : null;
|
|
|
}
|
|
|
|
|
|
+ private UserDO createUserInternal(String rawUsername, String rawPassword, String rawEmail, String rawRole) {
|
|
|
+ String username = normalizeUsername(rawUsername);
|
|
|
+ String email = normalizeText(rawEmail);
|
|
|
+ validateRegisterInput(username, rawPassword, email, rawRole);
|
|
|
+ UserRole role = UserRole.fromInput(rawRole);
|
|
|
+
|
|
|
+ UserDO existingUser = userDAO.findByUsername(username);
|
|
|
+ if (existingUser != null) {
|
|
|
+ throw new IllegalArgumentException("username already exists");
|
|
|
+ }
|
|
|
+
|
|
|
+ UserDO newUser = new UserDO();
|
|
|
+ newUser.setUsername(username);
|
|
|
+ newUser.setPasswordHash(passwordEncoder.encode(rawPassword));
|
|
|
+ newUser.setRole(role.getCode());
|
|
|
+ newUser.setEmail(email);
|
|
|
+ newUser.setStatus(USER_ENABLED);
|
|
|
+ newUser.setCreatedAt(LocalDateTime.now());
|
|
|
+ userDAO.save(newUser);
|
|
|
+ return newUser;
|
|
|
+ }
|
|
|
+
|
|
|
+ private UserDO requireUserExists(Long userId) {
|
|
|
+ UserDO target = userDAO.findByUserId(userId);
|
|
|
+ if (target == null) {
|
|
|
+ throw new IllegalArgumentException("User does not exist");
|
|
|
+ }
|
|
|
+ return target;
|
|
|
+ }
|
|
|
+
|
|
|
+ private UserLoginResp buildLoginResp(LoginUserBO loginUser) {
|
|
|
+ UserLoginResp resp = new UserLoginResp();
|
|
|
+ resp.setToken(loginUser.getToken());
|
|
|
+ resp.setExpiresAt(loginUser.getExpiresAt());
|
|
|
+ resp.setUserId(loginUser.getUserId());
|
|
|
+ resp.setUsername(loginUser.getUsername());
|
|
|
+ resp.setRole(loginUser.getRole());
|
|
|
+ resp.setEmail(loginUser.getEmail());
|
|
|
+ return resp;
|
|
|
+ }
|
|
|
+
|
|
|
+ private UserProfileResp toUserProfile(LoginUserBO loginUser, Integer userStatus) {
|
|
|
+ UserProfileResp profileResp = new UserProfileResp();
|
|
|
+ profileResp.setUserId(loginUser.getUserId());
|
|
|
+ profileResp.setUsername(loginUser.getUsername());
|
|
|
+ profileResp.setRole(loginUser.getRole());
|
|
|
+ profileResp.setEmail(loginUser.getEmail());
|
|
|
+ profileResp.setStatus(userStatus);
|
|
|
+ return profileResp;
|
|
|
+ }
|
|
|
+
|
|
|
+ private UserProfileResp toUserProfile(UserDO userDO) {
|
|
|
+ UserProfileResp profileResp = new UserProfileResp();
|
|
|
+ profileResp.setUserId(userDO.getUserId());
|
|
|
+ profileResp.setUsername(userDO.getUsername());
|
|
|
+ profileResp.setRole(userDO.getRole());
|
|
|
+ profileResp.setEmail(userDO.getEmail());
|
|
|
+ profileResp.setStatus(userDO.getStatus());
|
|
|
+ return profileResp;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Integer resolveUserStatus(Long userId) {
|
|
|
+ UserDO user = userDAO.findByUserId(userId);
|
|
|
+ return user == null ? USER_ENABLED : user.getStatus();
|
|
|
+ }
|
|
|
+
|
|
|
+ private Integer normalizeStatus(Integer status) {
|
|
|
+ if (status == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return status > 0 ? 1 : 0;
|
|
|
+ }
|
|
|
+
|
|
|
private String normalizeUsername(String username) {
|
|
|
if (!StringUtils.hasText(username)) {
|
|
|
return null;
|
|
|
@@ -195,7 +391,7 @@ public class AuthService {
|
|
|
return text.trim();
|
|
|
}
|
|
|
|
|
|
- private UserRole validateRegisterInput(String username, String password, String email, String roleText) {
|
|
|
+ private void validateRegisterInput(String username, String password, String email, String roleText) {
|
|
|
if (!StringUtils.hasText(username)) {
|
|
|
throw new IllegalArgumentException("username cannot be empty");
|
|
|
}
|
|
|
@@ -203,6 +399,19 @@ public class AuthService {
|
|
|
throw new IllegalArgumentException("username length exceeds " + USERNAME_MAX_LENGTH);
|
|
|
}
|
|
|
|
|
|
+ validatePassword(password);
|
|
|
+
|
|
|
+ if (StringUtils.hasText(email) && email.length() > EMAIL_MAX_LENGTH) {
|
|
|
+ throw new IllegalArgumentException("email length exceeds " + EMAIL_MAX_LENGTH);
|
|
|
+ }
|
|
|
+
|
|
|
+ UserRole role = UserRole.fromInput(roleText);
|
|
|
+ if (role == null) {
|
|
|
+ throw new IllegalArgumentException("role must be ADMIN, DEVELOPER or ANALYST");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validatePassword(String password) {
|
|
|
if (!StringUtils.hasText(password)) {
|
|
|
throw new IllegalArgumentException("password cannot be empty");
|
|
|
}
|
|
|
@@ -210,16 +419,23 @@ public class AuthService {
|
|
|
throw new IllegalArgumentException(
|
|
|
"password length must be between " + PASSWORD_MIN_LENGTH + " and " + PASSWORD_MAX_LENGTH);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (StringUtils.hasText(email) && email.length() > EMAIL_MAX_LENGTH) {
|
|
|
- throw new IllegalArgumentException("email length exceeds " + EMAIL_MAX_LENGTH);
|
|
|
+ private UserRole resolveInviteRole(OrganizationDO organization, String inviteCode) {
|
|
|
+ String analystCode = normalizeText(organization.getAnalystInviteCode());
|
|
|
+ String developerCode = normalizeText(organization.getDeveloperInviteCode());
|
|
|
+ if (inviteCode.equals(analystCode)) {
|
|
|
+ return UserRole.ANALYST;
|
|
|
}
|
|
|
-
|
|
|
- UserRole role = UserRole.fromInput(roleText);
|
|
|
- if (role == null) {
|
|
|
- throw new IllegalArgumentException("role must be ANALYST or OPS");
|
|
|
+ if (inviteCode.equals(developerCode)) {
|
|
|
+ return UserRole.DEVELOPER;
|
|
|
}
|
|
|
- return role;
|
|
|
+ throw new IllegalArgumentException("invalid inviteCode");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String generateInviteCode(String rolePrefix) {
|
|
|
+ String randomPart = UUID.randomUUID().toString().replace("-", "").substring(0, 10).toUpperCase();
|
|
|
+ return rolePrefix + "-" + randomPart;
|
|
|
}
|
|
|
|
|
|
private boolean isPasswordValid(String rawPassword, String passwordHash) {
|