用户/账号管理,系统日志
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package com.mzh.library.service.impl;
|
||||
|
||||
import com.mzh.library.dao.SystemLogDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.Permission;
|
||||
import com.mzh.library.entity.SystemLogPage;
|
||||
import com.mzh.library.entity.SystemLogSearchCriteria;
|
||||
import com.mzh.library.exception.DaoException;
|
||||
import com.mzh.library.service.PermissionPolicy;
|
||||
import com.mzh.library.service.ServiceResult;
|
||||
import com.mzh.library.service.SystemLogService;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class SystemLogServiceImpl implements SystemLogService {
|
||||
private static final Logger LOGGER = Logger.getLogger(SystemLogServiceImpl.class.getName());
|
||||
private static final String UNAVAILABLE_MESSAGE =
|
||||
"System log service is temporarily unavailable. Please try again later.";
|
||||
private static final String DENIED_MESSAGE = "You do not have permission to view system logs.";
|
||||
private static final String VALIDATION_MESSAGE = "Please correct the system log search filters.";
|
||||
|
||||
private final SystemLogDao systemLogDao;
|
||||
private final PermissionPolicy permissionPolicy;
|
||||
|
||||
public SystemLogServiceImpl(SystemLogDao systemLogDao) {
|
||||
this(systemLogDao, new PermissionPolicy());
|
||||
}
|
||||
|
||||
public SystemLogServiceImpl(SystemLogDao systemLogDao, PermissionPolicy permissionPolicy) {
|
||||
this.systemLogDao = systemLogDao;
|
||||
this.permissionPolicy = permissionPolicy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceResult<SystemLogPage> searchLogs(AuthenticatedUser actor, SystemLogSearchCriteria criteria) {
|
||||
if (!canViewSystemLogs(actor)) {
|
||||
return ServiceResult.failure(DENIED_MESSAGE);
|
||||
}
|
||||
|
||||
SystemLogSearchCriteria normalized = criteria == null ? new SystemLogSearchCriteria() : criteria;
|
||||
Map<String, String> errors = validate(normalized);
|
||||
if (!errors.isEmpty()) {
|
||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||
}
|
||||
|
||||
try {
|
||||
SystemLogPage page = new SystemLogPage();
|
||||
page.setLogs(systemLogDao.search(normalized));
|
||||
page.setOperationTypes(systemLogDao.findOperationTypes());
|
||||
return ServiceResult.success(page);
|
||||
} catch (DaoException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Unable to load system logs actorId=" + actor.getId(), ex);
|
||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> validate(SystemLogSearchCriteria criteria) {
|
||||
Map<String, String> errors = new LinkedHashMap<>();
|
||||
if (criteria.getOperationType().length() > 64) {
|
||||
errors.put("operationType", "Operation type must be 64 characters or fewer.");
|
||||
}
|
||||
if (criteria.getKeyword().length() > 120) {
|
||||
errors.put("keyword", "Keyword must be 120 characters or fewer.");
|
||||
}
|
||||
|
||||
parseDate(criteria.getCreatedFromText(), "createdFrom", "Start date", errors, criteria, true);
|
||||
parseDate(criteria.getCreatedToText(), "createdTo", "End date", errors, criteria, false);
|
||||
if (criteria.getCreatedFrom() != null
|
||||
&& criteria.getCreatedTo() != null
|
||||
&& criteria.getCreatedFrom().isAfter(criteria.getCreatedTo())) {
|
||||
errors.put("createdTo", "End date must be on or after start date.");
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
private void parseDate(String value, String field, String label, Map<String, String> errors,
|
||||
SystemLogSearchCriteria criteria, boolean fromDate) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
LocalDate parsed = LocalDate.parse(value);
|
||||
if (fromDate) {
|
||||
criteria.setCreatedFrom(parsed);
|
||||
} else {
|
||||
criteria.setCreatedTo(parsed);
|
||||
}
|
||||
} catch (DateTimeParseException ex) {
|
||||
errors.put(field, label + " must use YYYY-MM-DD.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canViewSystemLogs(AuthenticatedUser actor) {
|
||||
return actor != null && permissionPolicy.allows(actor.getRole(), Permission.VIEW_SYSTEM_LOGS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.mzh.library.service.impl;
|
||||
|
||||
import com.mzh.library.dao.SystemLogDao;
|
||||
import com.mzh.library.dao.UserAccountDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.Permission;
|
||||
import com.mzh.library.entity.Role;
|
||||
import com.mzh.library.entity.SystemLog;
|
||||
import com.mzh.library.entity.User;
|
||||
import com.mzh.library.entity.UserSearchCriteria;
|
||||
import com.mzh.library.exception.DaoException;
|
||||
import com.mzh.library.service.PermissionPolicy;
|
||||
import com.mzh.library.service.ServiceResult;
|
||||
import com.mzh.library.service.UserAccountService;
|
||||
import com.mzh.library.util.JdbcUtil;
|
||||
import com.mzh.library.util.PasswordHasher;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class UserAccountServiceImpl implements UserAccountService {
|
||||
public interface TransactionExecutor {
|
||||
<T> T execute(JdbcUtil.TransactionCallback<T> callback);
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(UserAccountServiceImpl.class.getName());
|
||||
private static final String UNAVAILABLE_MESSAGE =
|
||||
"User management service is temporarily unavailable. Please try again later.";
|
||||
private static final String VALIDATION_MESSAGE = "Please correct the highlighted account fields.";
|
||||
private static final String SEARCH_VALIDATION_MESSAGE = "Please correct the account search filters.";
|
||||
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
|
||||
private static final String SELF_DEACTIVATE_MESSAGE = "You cannot deactivate your own administrator account.";
|
||||
private static final String SELF_ROLE_MESSAGE = "You cannot change your own administrator role.";
|
||||
|
||||
private final UserAccountDao userAccountDao;
|
||||
private final SystemLogDao systemLogDao;
|
||||
private final PermissionPolicy permissionPolicy;
|
||||
private final TransactionExecutor transactionExecutor;
|
||||
|
||||
public UserAccountServiceImpl(UserAccountDao userAccountDao, SystemLogDao systemLogDao) {
|
||||
this(userAccountDao, systemLogDao, new PermissionPolicy(), new JdbcTransactionExecutor());
|
||||
}
|
||||
|
||||
public UserAccountServiceImpl(UserAccountDao userAccountDao, SystemLogDao systemLogDao,
|
||||
PermissionPolicy permissionPolicy, TransactionExecutor transactionExecutor) {
|
||||
this.userAccountDao = userAccountDao;
|
||||
this.systemLogDao = systemLogDao;
|
||||
this.permissionPolicy = permissionPolicy;
|
||||
this.transactionExecutor = transactionExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceResult<List<User>> searchUsers(AuthenticatedUser actor, UserSearchCriteria criteria) {
|
||||
if (!canManageUsers(actor)) {
|
||||
return ServiceResult.failure(DENIED_MESSAGE);
|
||||
}
|
||||
|
||||
UserSearchCriteria normalized = criteria == null ? new UserSearchCriteria() : criteria;
|
||||
Map<String, String> errors = validateSearch(normalized);
|
||||
if (!errors.isEmpty()) {
|
||||
return ServiceResult.validationFailure(SEARCH_VALIDATION_MESSAGE, errors);
|
||||
}
|
||||
|
||||
try {
|
||||
return ServiceResult.success(userAccountDao.search(normalized));
|
||||
} catch (DaoException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Unable to search users actorId=" + actor.getId(), ex);
|
||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceResult<Optional<User>> findUser(AuthenticatedUser actor, long id) {
|
||||
if (!canManageUsers(actor)) {
|
||||
return ServiceResult.failure(DENIED_MESSAGE);
|
||||
}
|
||||
if (id <= 0) {
|
||||
return ServiceResult.failure("Select a valid user account.");
|
||||
}
|
||||
|
||||
try {
|
||||
return ServiceResult.success(userAccountDao.findById(id));
|
||||
} catch (DaoException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Unable to load user id=" + id + " actorId=" + actor.getId(), ex);
|
||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceResult<Long> createUser(AuthenticatedUser actor, User user, String password, String requestIp) {
|
||||
if (!canManageUsers(actor)) {
|
||||
return ServiceResult.failure(DENIED_MESSAGE);
|
||||
}
|
||||
|
||||
normalize(user);
|
||||
Map<String, String> errors = validateUser(user, false, password, true);
|
||||
if (!errors.isEmpty()) {
|
||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||
}
|
||||
|
||||
try {
|
||||
if (userAccountDao.findByUsername(user.getUsername()).isPresent()) {
|
||||
errors.put("username", "Username is already in use.");
|
||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||
}
|
||||
|
||||
user.setPasswordHash(PasswordHasher.hash(password));
|
||||
return transactionExecutor.execute(connection -> {
|
||||
long id = userAccountDao.create(connection, user);
|
||||
systemLogDao.create(connection, auditLog(actor, "user.create", id,
|
||||
"Created account username=" + user.getUsername() + " role=" + user.getRole().getCode(),
|
||||
requestIp));
|
||||
LOGGER.info("Created user id=" + id + " actorId=" + actor.getId());
|
||||
return ServiceResult.success(id, "User account created.");
|
||||
});
|
||||
} catch (DaoException | IllegalStateException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Unable to create user actorId=" + actor.getId()
|
||||
+ " username=" + safeUsername(user), ex);
|
||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceResult<Void> updateUser(AuthenticatedUser actor, User user, String password, String requestIp) {
|
||||
if (!canManageUsers(actor)) {
|
||||
return ServiceResult.failure(DENIED_MESSAGE);
|
||||
}
|
||||
|
||||
normalize(user);
|
||||
Map<String, String> errors = validateUser(user, true, password, false);
|
||||
if (!errors.isEmpty()) {
|
||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||
}
|
||||
|
||||
try {
|
||||
Optional<User> existingResult = userAccountDao.findById(user.getId());
|
||||
if (!existingResult.isPresent()) {
|
||||
return ServiceResult.failure("User account was not found.");
|
||||
}
|
||||
|
||||
protectCurrentAdministrator(actor, user, errors);
|
||||
if (!errors.isEmpty()) {
|
||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||
}
|
||||
|
||||
User existing = existingResult.get();
|
||||
user.setUsername(existing.getUsername());
|
||||
boolean updatePassword = password != null && !password.trim().isEmpty();
|
||||
if (updatePassword) {
|
||||
user.setPasswordHash(PasswordHasher.hash(password));
|
||||
}
|
||||
|
||||
final boolean passwordChanged = updatePassword;
|
||||
return transactionExecutor.execute(connection -> {
|
||||
if (!userAccountDao.update(connection, user, passwordChanged)) {
|
||||
return ServiceResult.failure("User account was not found.");
|
||||
}
|
||||
systemLogDao.create(connection, auditLog(actor, "user.update", user.getId(),
|
||||
"Updated account username=" + user.getUsername() + " role=" + user.getRole().getCode()
|
||||
+ " active=" + user.isActive()
|
||||
+ (passwordChanged ? " passwordReset=true" : ""),
|
||||
requestIp));
|
||||
LOGGER.info("Updated user id=" + user.getId() + " actorId=" + actor.getId());
|
||||
return ServiceResult.success(null, "User account updated.");
|
||||
});
|
||||
} catch (DaoException | IllegalStateException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Unable to update user id=" + user.getId() + " actorId=" + actor.getId(), ex);
|
||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceResult<Void> deactivateUser(AuthenticatedUser actor, long id, String requestIp) {
|
||||
if (!canManageUsers(actor)) {
|
||||
return ServiceResult.failure(DENIED_MESSAGE);
|
||||
}
|
||||
if (id <= 0) {
|
||||
return ServiceResult.failure("Select a valid user account.");
|
||||
}
|
||||
if (actor.getId() == id) {
|
||||
Map<String, String> errors = new LinkedHashMap<>();
|
||||
errors.put("active", SELF_DEACTIVATE_MESSAGE);
|
||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||
}
|
||||
|
||||
try {
|
||||
Optional<User> existingResult = userAccountDao.findById(id);
|
||||
if (!existingResult.isPresent()) {
|
||||
return ServiceResult.failure("User account was not found.");
|
||||
}
|
||||
|
||||
User user = existingResult.get();
|
||||
user.setActive(false);
|
||||
return transactionExecutor.execute(connection -> {
|
||||
if (!userAccountDao.update(connection, user, false)) {
|
||||
return ServiceResult.failure("User account was not found.");
|
||||
}
|
||||
systemLogDao.create(connection, auditLog(actor, "user.deactivate", id,
|
||||
"Deactivated account username=" + user.getUsername(),
|
||||
requestIp));
|
||||
LOGGER.info("Deactivated user id=" + id + " actorId=" + actor.getId());
|
||||
return ServiceResult.success(null, "User account deactivated.");
|
||||
});
|
||||
} catch (DaoException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Unable to deactivate user id=" + id + " actorId=" + actor.getId(), ex);
|
||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> validateSearch(UserSearchCriteria criteria) {
|
||||
Map<String, String> errors = new LinkedHashMap<>();
|
||||
if (!criteria.getRoleCode().isEmpty()) {
|
||||
try {
|
||||
criteria.setRoleCode(Role.fromCode(criteria.getRoleCode()).getCode());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
errors.put("role", "Select a valid role.");
|
||||
}
|
||||
}
|
||||
|
||||
String activeStatus = criteria.getActiveStatus();
|
||||
if (!activeStatus.isEmpty()
|
||||
&& !UserSearchCriteria.ACTIVE_STATUS.equals(activeStatus)
|
||||
&& !UserSearchCriteria.INACTIVE_STATUS.equals(activeStatus)) {
|
||||
errors.put("active", "Select a valid active state.");
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
private Map<String, String> validateUser(User user, boolean requireId, String password, boolean requirePassword) {
|
||||
Map<String, String> errors = new LinkedHashMap<>();
|
||||
if (user == null) {
|
||||
errors.put("user", "User account details are required.");
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (requireId && user.getId() <= 0) {
|
||||
errors.put("id", "Select a valid user account.");
|
||||
}
|
||||
if (!requireId) {
|
||||
requireLength(errors, "username", user.getUsername(), "Username", 64);
|
||||
}
|
||||
requireLength(errors, "displayName", user.getDisplayName(), "Display name", 100);
|
||||
if (user.getRole() == null) {
|
||||
errors.put("role", "Select a role.");
|
||||
}
|
||||
validatePassword(errors, password, requirePassword);
|
||||
return errors;
|
||||
}
|
||||
|
||||
private void validatePassword(Map<String, String> errors, String password, boolean required) {
|
||||
String trimmed = password == null ? "" : password.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
if (required) {
|
||||
errors.put("password", "Password is required.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (password.length() > 128) {
|
||||
errors.put("password", "Password must be 128 characters or fewer.");
|
||||
}
|
||||
}
|
||||
|
||||
private void protectCurrentAdministrator(AuthenticatedUser actor, User user, Map<String, String> errors) {
|
||||
if (actor.getId() != user.getId()) {
|
||||
return;
|
||||
}
|
||||
if (!user.isActive()) {
|
||||
errors.put("active", SELF_DEACTIVATE_MESSAGE);
|
||||
}
|
||||
if (user.getRole() != Role.ADMINISTRATOR) {
|
||||
errors.put("role", SELF_ROLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
errors.put(field, label + " is required.");
|
||||
return;
|
||||
}
|
||||
if (value.length() > maxLength) {
|
||||
errors.put(field, label + " must be " + maxLength + " characters or fewer.");
|
||||
}
|
||||
}
|
||||
|
||||
private SystemLog auditLog(AuthenticatedUser actor, String operationType, long userId, String message,
|
||||
String requestIp) {
|
||||
SystemLog log = new SystemLog();
|
||||
log.setOperatorId(actor.getId());
|
||||
log.setOperatorRole(actor.getRole().getCode());
|
||||
log.setOperationType(operationType);
|
||||
log.setTargetTable("users");
|
||||
log.setTargetId(String.valueOf(userId));
|
||||
log.setResultStatus("success");
|
||||
log.setMessage(message);
|
||||
log.setRequestIp(trim(requestIp));
|
||||
return log;
|
||||
}
|
||||
|
||||
private boolean canManageUsers(AuthenticatedUser actor) {
|
||||
return actor != null && permissionPolicy.allows(actor.getRole(), Permission.MANAGE_USERS);
|
||||
}
|
||||
|
||||
private void normalize(User user) {
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
user.setUsername(normalizeUsername(user.getUsername()));
|
||||
user.setDisplayName(trim(user.getDisplayName()));
|
||||
}
|
||||
|
||||
private String normalizeUsername(String username) {
|
||||
return trim(username);
|
||||
}
|
||||
|
||||
private String safeUsername(User user) {
|
||||
return user == null ? "" : user.getUsername();
|
||||
}
|
||||
|
||||
private String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static final class JdbcTransactionExecutor implements TransactionExecutor {
|
||||
@Override
|
||||
public <T> T execute(JdbcUtil.TransactionCallback<T> callback) {
|
||||
return JdbcUtil.executeInTransaction(callback);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DirectTransactionExecutor implements TransactionExecutor {
|
||||
@Override
|
||||
public <T> T execute(JdbcUtil.TransactionCallback<T> callback) {
|
||||
try {
|
||||
return callback.execute(null);
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to execute direct transaction", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user