用户/账号管理,系统日志
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package com.mzh.library.controller;
|
||||
|
||||
import com.mzh.library.dao.impl.JdbcSystemLogDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.SystemLogPage;
|
||||
import com.mzh.library.entity.SystemLogSearchCriteria;
|
||||
import com.mzh.library.service.ServiceResult;
|
||||
import com.mzh.library.service.SystemLogService;
|
||||
import com.mzh.library.service.impl.SystemLogServiceImpl;
|
||||
import com.mzh.library.util.SessionAttributes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
public class SystemLogServlet extends HttpServlet {
|
||||
private static final String LOGS_JSP = "/WEB-INF/jsp/maintenance/system-logs.jsp";
|
||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||
private static final String DENIED_MESSAGE = "You do not have permission to view system logs.";
|
||||
|
||||
private SystemLogService systemLogService;
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
this.systemLogService = new SystemLogServiceImpl(new JdbcSystemLogDao());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
SystemLogSearchCriteria criteria = new SystemLogSearchCriteria(
|
||||
request.getParameter("operationType"),
|
||||
request.getParameter("keyword"),
|
||||
request.getParameter("createdFrom"),
|
||||
request.getParameter("createdTo")
|
||||
);
|
||||
request.setAttribute("criteria", criteria);
|
||||
|
||||
ServiceResult<SystemLogPage> result = systemLogService.searchLogs(currentUser(request), criteria);
|
||||
if (isPermissionDenied(result)) {
|
||||
forwardDenied(request, response, result.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.isSuccessful()) {
|
||||
request.setAttribute("logs", result.getData().getLogs());
|
||||
request.setAttribute("operationTypes", result.getData().getOperationTypes());
|
||||
} else {
|
||||
request.setAttribute("logs", Collections.emptyList());
|
||||
request.setAttribute("operationTypes", Collections.emptyList());
|
||||
request.setAttribute("errorMessage", result.getMessage());
|
||||
request.setAttribute("errors", result.getErrors());
|
||||
}
|
||||
|
||||
request.getRequestDispatcher(LOGS_JSP).forward(request, response);
|
||||
}
|
||||
|
||||
private boolean isPermissionDenied(ServiceResult<?> result) {
|
||||
return !result.isSuccessful() && DENIED_MESSAGE.equals(result.getMessage());
|
||||
}
|
||||
|
||||
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
||||
throws ServletException, IOException {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
request.setAttribute("errorMessage", message);
|
||||
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
|
||||
}
|
||||
|
||||
private AuthenticatedUser currentUser(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
Object value = session == null ? null : session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
|
||||
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package com.mzh.library.controller;
|
||||
|
||||
import com.mzh.library.dao.impl.JdbcSystemLogDao;
|
||||
import com.mzh.library.dao.impl.JdbcUserDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.Role;
|
||||
import com.mzh.library.entity.User;
|
||||
import com.mzh.library.entity.UserSearchCriteria;
|
||||
import com.mzh.library.service.ServiceResult;
|
||||
import com.mzh.library.service.UserAccountService;
|
||||
import com.mzh.library.service.impl.UserAccountServiceImpl;
|
||||
import com.mzh.library.util.SessionAttributes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
public class UserManagementServlet extends HttpServlet {
|
||||
private static final String MANAGE_JSP = "/WEB-INF/jsp/admin/users/manage.jsp";
|
||||
private static final String FORM_JSP = "/WEB-INF/jsp/admin/users/form.jsp";
|
||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||
private static final String FLASH_SUCCESS = "flashSuccess";
|
||||
private static final String FLASH_ERROR = "flashError";
|
||||
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
|
||||
|
||||
private UserAccountService userAccountService;
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
JdbcUserDao userDao = new JdbcUserDao();
|
||||
this.userAccountService = new UserAccountServiceImpl(userDao, new JdbcSystemLogDao());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
String path = request.getServletPath();
|
||||
if ("/admin/users/new".equals(path)) {
|
||||
renderForm(request, response, "Create user account", "/admin/users", defaultUser(),
|
||||
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||
return;
|
||||
}
|
||||
if ("/admin/users/edit".equals(path)) {
|
||||
showEditForm(request, response);
|
||||
return;
|
||||
}
|
||||
if (!"/admin/users".equals(path)) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
showManagementList(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
String path = request.getServletPath();
|
||||
if ("/admin/users".equals(path)) {
|
||||
createUser(request, response);
|
||||
return;
|
||||
}
|
||||
if ("/admin/users/update".equals(path)) {
|
||||
updateUser(request, response);
|
||||
return;
|
||||
}
|
||||
if ("/admin/users/deactivate".equals(path)) {
|
||||
deactivateUser(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
|
||||
private void showManagementList(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
UserSearchCriteria criteria = searchCriteria(request);
|
||||
request.setAttribute("criteria", criteria);
|
||||
request.setAttribute("roles", Role.values());
|
||||
applyFlash(request);
|
||||
|
||||
ServiceResult<List<User>> result = userAccountService.searchUsers(currentUser(request), criteria);
|
||||
if (isPermissionDenied(result)) {
|
||||
forwardDenied(request, response, result.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
request.setAttribute("users", result.isSuccessful() ? result.getData() : Collections.emptyList());
|
||||
if (!result.isSuccessful()) {
|
||||
request.setAttribute("errorMessage", result.getMessage());
|
||||
request.setAttribute("errors", result.getErrors());
|
||||
}
|
||||
request.getRequestDispatcher(MANAGE_JSP).forward(request, response);
|
||||
}
|
||||
|
||||
private void showEditForm(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
long id = requiredLong(request.getParameter("id"), -1L);
|
||||
ServiceResult<Optional<User>> result = userAccountService.findUser(currentUser(request), id);
|
||||
if (isPermissionDenied(result)) {
|
||||
forwardDenied(request, response, result.getMessage());
|
||||
return;
|
||||
}
|
||||
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
||||
flashError(request, result.isSuccessful() ? "User account was not found." : result.getMessage());
|
||||
response.sendRedirect(request.getContextPath() + "/admin/users");
|
||||
return;
|
||||
}
|
||||
|
||||
renderForm(request, response, "Edit user account", "/admin/users/update", result.getData().get(),
|
||||
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||
}
|
||||
|
||||
private void createUser(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
UserForm form = readUserForm(request, false);
|
||||
if (!form.getErrors().isEmpty()) {
|
||||
renderForm(request, response, "Create user account", "/admin/users", form.getUser(), form.getValues(),
|
||||
form.getErrors(), "Please correct the highlighted account fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
ServiceResult<Long> result = userAccountService.createUser(currentUser(request), form.getUser(),
|
||||
form.getPassword(), clientIp(request));
|
||||
if (!result.isSuccessful()) {
|
||||
handleFormFailure(request, response, "Create user account", "/admin/users", form, result);
|
||||
return;
|
||||
}
|
||||
|
||||
flashSuccess(request, result.getMessage());
|
||||
response.sendRedirect(request.getContextPath() + "/admin/users");
|
||||
}
|
||||
|
||||
private void updateUser(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
UserForm form = readUserForm(request, true);
|
||||
if (!form.getErrors().isEmpty()) {
|
||||
renderForm(request, response, "Edit user account", "/admin/users/update", form.getUser(), form.getValues(),
|
||||
form.getErrors(), "Please correct the highlighted account fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
ServiceResult<Void> result = userAccountService.updateUser(currentUser(request), form.getUser(),
|
||||
form.getPassword(), clientIp(request));
|
||||
if (!result.isSuccessful()) {
|
||||
handleFormFailure(request, response, "Edit user account", "/admin/users/update", form, result);
|
||||
return;
|
||||
}
|
||||
|
||||
flashSuccess(request, result.getMessage());
|
||||
response.sendRedirect(request.getContextPath() + "/admin/users");
|
||||
}
|
||||
|
||||
private void deactivateUser(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
long id = requiredLong(request.getParameter("id"), -1L);
|
||||
ServiceResult<Void> result = userAccountService.deactivateUser(currentUser(request), id, clientIp(request));
|
||||
if (isPermissionDenied(result)) {
|
||||
forwardDenied(request, response, result.getMessage());
|
||||
return;
|
||||
}
|
||||
if (result.isSuccessful()) {
|
||||
flashSuccess(request, result.getMessage());
|
||||
} else {
|
||||
flashError(request, result.getMessage());
|
||||
}
|
||||
response.sendRedirect(request.getContextPath() + "/admin/users");
|
||||
}
|
||||
|
||||
private void handleFormFailure(HttpServletRequest request, HttpServletResponse response, String title,
|
||||
String action, UserForm form, ServiceResult<?> result)
|
||||
throws ServletException, IOException {
|
||||
if (isPermissionDenied(result)) {
|
||||
forwardDenied(request, response, result.getMessage());
|
||||
return;
|
||||
}
|
||||
renderForm(request, response, title, action, form.getUser(), form.getValues(), result.getErrors(),
|
||||
result.getMessage());
|
||||
}
|
||||
|
||||
private void renderForm(HttpServletRequest request, HttpServletResponse response, String title, String action,
|
||||
User user, Map<String, String> formValues, Map<String, String> errors,
|
||||
String errorMessage)
|
||||
throws ServletException, IOException {
|
||||
request.setAttribute("roles", Role.values());
|
||||
request.setAttribute("formTitle", title);
|
||||
request.setAttribute("formAction", action);
|
||||
request.setAttribute("user", user);
|
||||
request.setAttribute("formValues", formValues);
|
||||
request.setAttribute("errors", errors);
|
||||
if (errorMessage != null && !errorMessage.isEmpty()) {
|
||||
request.setAttribute("errorMessage", errorMessage);
|
||||
}
|
||||
request.getRequestDispatcher(FORM_JSP).forward(request, response);
|
||||
}
|
||||
|
||||
private UserForm readUserForm(HttpServletRequest request, boolean requireId) {
|
||||
Map<String, String> values = formValues(request);
|
||||
Map<String, String> errors = new LinkedHashMap<>();
|
||||
User user = new User();
|
||||
|
||||
if (requireId) {
|
||||
user.setId(parseLong(values.get("id"), "id", "Select a valid user account.", errors));
|
||||
}
|
||||
user.setUsername(values.get("username"));
|
||||
user.setDisplayName(values.get("displayName"));
|
||||
user.setActive(parseActive(values.get("active"), errors));
|
||||
try {
|
||||
user.setRole(Role.fromCode(values.get("role")));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
errors.put("role", "Select a role.");
|
||||
}
|
||||
|
||||
return new UserForm(user, values, errors, request.getParameter("password"));
|
||||
}
|
||||
|
||||
private Map<String, String> formValues(HttpServletRequest request) {
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
values.put("id", trim(request.getParameter("id")));
|
||||
values.put("username", trim(request.getParameter("username")));
|
||||
values.put("displayName", trim(request.getParameter("displayName")));
|
||||
values.put("role", trim(request.getParameter("role")));
|
||||
values.put("active", trim(request.getParameter("active")));
|
||||
return values;
|
||||
}
|
||||
|
||||
private UserSearchCriteria searchCriteria(HttpServletRequest request) {
|
||||
return new UserSearchCriteria(
|
||||
request.getParameter("keyword"),
|
||||
request.getParameter("role"),
|
||||
request.getParameter("active")
|
||||
);
|
||||
}
|
||||
|
||||
private User defaultUser() {
|
||||
User user = new User();
|
||||
user.setRole(Role.READER);
|
||||
user.setActive(true);
|
||||
return user;
|
||||
}
|
||||
|
||||
private boolean parseActive(String value, Map<String, String> errors) {
|
||||
String normalized = trim(value);
|
||||
if ("true".equals(normalized) || UserSearchCriteria.ACTIVE_STATUS.equals(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if ("false".equals(normalized) || UserSearchCriteria.INACTIVE_STATUS.equals(normalized)) {
|
||||
return false;
|
||||
}
|
||||
errors.put("active", "Select an active state.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private long parseLong(String value, String field, String message, Map<String, String> errors) {
|
||||
String trimmed = trim(value);
|
||||
if (trimmed.isEmpty()) {
|
||||
errors.put(field, message);
|
||||
return 0L;
|
||||
}
|
||||
try {
|
||||
long parsed = Long.parseLong(trimmed);
|
||||
if (parsed <= 0) {
|
||||
errors.put(field, message);
|
||||
}
|
||||
return parsed;
|
||||
} catch (NumberFormatException ex) {
|
||||
errors.put(field, message);
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private long requiredLong(String value, long fallback) {
|
||||
try {
|
||||
long parsed = Long.parseLong(trim(value));
|
||||
return parsed > 0 ? parsed : fallback;
|
||||
} catch (NumberFormatException ex) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPermissionDenied(ServiceResult<?> result) {
|
||||
return !result.isSuccessful() && DENIED_MESSAGE.equals(result.getMessage());
|
||||
}
|
||||
|
||||
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
||||
throws ServletException, IOException {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
request.setAttribute("errorMessage", message);
|
||||
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
|
||||
}
|
||||
|
||||
private AuthenticatedUser currentUser(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
Object value = session == null ? null : session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
|
||||
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
|
||||
}
|
||||
|
||||
private void applyFlash(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
moveFlash(session, request, FLASH_SUCCESS, "successMessage");
|
||||
moveFlash(session, request, FLASH_ERROR, "errorMessage");
|
||||
}
|
||||
|
||||
private void moveFlash(HttpSession session, HttpServletRequest request, String sessionKey, String requestKey) {
|
||||
Object value = session.getAttribute(sessionKey);
|
||||
if (value != null) {
|
||||
request.setAttribute(requestKey, value);
|
||||
session.removeAttribute(sessionKey);
|
||||
}
|
||||
}
|
||||
|
||||
private void flashSuccess(HttpServletRequest request, String message) {
|
||||
request.getSession().setAttribute(FLASH_SUCCESS, message);
|
||||
}
|
||||
|
||||
private void flashError(HttpServletRequest request, String message) {
|
||||
request.getSession().setAttribute(FLASH_ERROR, message);
|
||||
}
|
||||
|
||||
private String clientIp(HttpServletRequest request) {
|
||||
return trim(request.getRemoteAddr());
|
||||
}
|
||||
|
||||
private String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static final class UserForm {
|
||||
private final User user;
|
||||
private final Map<String, String> values;
|
||||
private final Map<String, String> errors;
|
||||
private final String password;
|
||||
|
||||
private UserForm(User user, Map<String, String> values, Map<String, String> errors, String password) {
|
||||
this.user = user;
|
||||
this.values = values;
|
||||
this.errors = errors;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
private User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
private Map<String, String> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
private Map<String, String> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
private String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.mzh.library.dao;
|
||||
|
||||
import com.mzh.library.entity.SystemLog;
|
||||
import com.mzh.library.entity.SystemLogSearchCriteria;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
|
||||
public interface SystemLogDao {
|
||||
List<SystemLog> search(SystemLogSearchCriteria criteria);
|
||||
|
||||
List<String> findOperationTypes();
|
||||
|
||||
long create(Connection connection, SystemLog log);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.mzh.library.dao;
|
||||
|
||||
import com.mzh.library.entity.User;
|
||||
import com.mzh.library.entity.UserSearchCriteria;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserAccountDao {
|
||||
List<User> search(UserSearchCriteria criteria);
|
||||
|
||||
Optional<User> findById(long id);
|
||||
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
long create(Connection connection, User user);
|
||||
|
||||
boolean update(Connection connection, User user, boolean updatePassword);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.mzh.library.dao.impl;
|
||||
|
||||
import com.mzh.library.dao.SystemLogDao;
|
||||
import com.mzh.library.entity.SystemLog;
|
||||
import com.mzh.library.entity.SystemLogSearchCriteria;
|
||||
import com.mzh.library.exception.DaoException;
|
||||
import com.mzh.library.util.JdbcUtil;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.sql.Types;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class JdbcSystemLogDao implements SystemLogDao {
|
||||
private static final String LOG_COLUMNS = ""
|
||||
+ "sl.id, sl.operator_id, u.username AS operator_username, "
|
||||
+ "u.display_name AS operator_display_name, sl.operator_role, sl.operation_type, "
|
||||
+ "sl.target_table, sl.target_id, sl.result_status, sl.message, sl.request_ip, sl.created_at ";
|
||||
|
||||
private static final String LOG_FROM = ""
|
||||
+ "FROM system_logs sl "
|
||||
+ "LEFT JOIN users u ON u.id = sl.operator_id ";
|
||||
|
||||
private static final String OPERATION_TYPES = ""
|
||||
+ "SELECT DISTINCT operation_type "
|
||||
+ "FROM system_logs "
|
||||
+ "ORDER BY operation_type";
|
||||
|
||||
private static final String CREATE = ""
|
||||
+ "INSERT INTO system_logs "
|
||||
+ "(operator_id, operator_role, operation_type, target_table, target_id, result_status, message, request_ip) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
@Override
|
||||
public List<SystemLog> search(SystemLogSearchCriteria criteria) {
|
||||
SystemLogSearchCriteria normalized = criteria == null ? new SystemLogSearchCriteria() : criteria;
|
||||
List<Object> parameters = new ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder("SELECT ")
|
||||
.append(LOG_COLUMNS)
|
||||
.append(LOG_FROM)
|
||||
.append("WHERE 1 = 1 ");
|
||||
|
||||
if (!normalized.getOperationType().isEmpty()) {
|
||||
sql.append("AND sl.operation_type = ? ");
|
||||
parameters.add(normalized.getOperationType());
|
||||
}
|
||||
appendKeyword(sql, parameters, normalized.getKeyword());
|
||||
if (normalized.getCreatedFrom() != null) {
|
||||
sql.append("AND sl.created_at >= ? ");
|
||||
parameters.add(Timestamp.valueOf(normalized.getCreatedFrom().atStartOfDay()));
|
||||
}
|
||||
if (normalized.getCreatedTo() != null) {
|
||||
sql.append("AND sl.created_at < ? ");
|
||||
parameters.add(Timestamp.valueOf(normalized.getCreatedTo().plusDays(1).atStartOfDay()));
|
||||
}
|
||||
sql.append("ORDER BY sl.created_at DESC, sl.id DESC");
|
||||
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql.toString())) {
|
||||
bind(statement, parameters);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
List<SystemLog> logs = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
logs.add(mapLog(resultSet));
|
||||
}
|
||||
return logs;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to search system logs", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findOperationTypes() {
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(OPERATION_TYPES);
|
||||
ResultSet resultSet = statement.executeQuery()) {
|
||||
List<String> operationTypes = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
operationTypes.add(resultSet.getString("operation_type"));
|
||||
}
|
||||
return operationTypes;
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to load system log operation types", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long create(Connection connection, SystemLog log) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(CREATE, Statement.RETURN_GENERATED_KEYS)) {
|
||||
if (log.getOperatorId() == null) {
|
||||
statement.setNull(1, Types.BIGINT);
|
||||
} else {
|
||||
statement.setLong(1, log.getOperatorId());
|
||||
}
|
||||
statement.setString(2, log.getOperatorRole());
|
||||
statement.setString(3, log.getOperationType());
|
||||
statement.setString(4, log.getTargetTable());
|
||||
statement.setString(5, log.getTargetId());
|
||||
statement.setString(6, log.getResultStatus());
|
||||
statement.setString(7, log.getMessage());
|
||||
statement.setString(8, log.getRequestIp());
|
||||
statement.executeUpdate();
|
||||
|
||||
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
|
||||
if (generatedKeys.next()) {
|
||||
return generatedKeys.getLong(1);
|
||||
}
|
||||
}
|
||||
throw new DaoException("Unable to read generated system log id", null);
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to create system log", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendKeyword(StringBuilder sql, List<Object> parameters, String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String filter = "%" + value.trim() + "%";
|
||||
sql.append("AND (sl.operation_type LIKE ? OR sl.target_table LIKE ? OR sl.target_id LIKE ? ")
|
||||
.append("OR sl.message LIKE ? OR sl.request_ip LIKE ? OR u.username LIKE ? OR u.display_name LIKE ?) ");
|
||||
for (int i = 0; i < 7; i++) {
|
||||
parameters.add(filter);
|
||||
}
|
||||
}
|
||||
|
||||
private void bind(PreparedStatement statement, List<Object> parameters) throws SQLException {
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
Object value = parameters.get(i);
|
||||
if (value instanceof Timestamp) {
|
||||
statement.setTimestamp(i + 1, (Timestamp) value);
|
||||
} else {
|
||||
statement.setString(i + 1, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SystemLog mapLog(ResultSet resultSet) throws SQLException {
|
||||
SystemLog log = new SystemLog();
|
||||
log.setId(resultSet.getLong("id"));
|
||||
long operatorId = resultSet.getLong("operator_id");
|
||||
log.setOperatorId(resultSet.wasNull() ? null : operatorId);
|
||||
log.setOperatorUsername(resultSet.getString("operator_username"));
|
||||
log.setOperatorDisplayName(resultSet.getString("operator_display_name"));
|
||||
log.setOperatorRole(resultSet.getString("operator_role"));
|
||||
log.setOperationType(resultSet.getString("operation_type"));
|
||||
log.setTargetTable(resultSet.getString("target_table"));
|
||||
log.setTargetId(resultSet.getString("target_id"));
|
||||
log.setResultStatus(resultSet.getString("result_status"));
|
||||
log.setMessage(resultSet.getString("message"));
|
||||
log.setRequestIp(resultSet.getString("request_ip"));
|
||||
log.setCreatedAt(toLocalDateTime(resultSet.getTimestamp("created_at")));
|
||||
return log;
|
||||
}
|
||||
|
||||
private LocalDateTime toLocalDateTime(Timestamp timestamp) {
|
||||
return timestamp == null ? null : timestamp.toLocalDateTime();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.mzh.library.dao.impl;
|
||||
|
||||
import com.mzh.library.dao.UserAccountDao;
|
||||
import com.mzh.library.dao.UserDao;
|
||||
import com.mzh.library.entity.Role;
|
||||
import com.mzh.library.entity.User;
|
||||
import com.mzh.library.entity.UserSearchCriteria;
|
||||
import com.mzh.library.exception.DaoException;
|
||||
import com.mzh.library.util.JdbcUtil;
|
||||
|
||||
@@ -10,14 +12,40 @@ import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class JdbcUserDao implements UserDao {
|
||||
public class JdbcUserDao implements UserDao, UserAccountDao {
|
||||
private static final String USER_COLUMNS = ""
|
||||
+ "id, username, password_hash, display_name, role_code, active, created_at, updated_at ";
|
||||
|
||||
private static final String FIND_ACTIVE_BY_USERNAME = ""
|
||||
+ "SELECT id, username, password_hash, display_name, role_code, active "
|
||||
+ "SELECT " + USER_COLUMNS
|
||||
+ "FROM users "
|
||||
+ "WHERE username = ? AND active = 1";
|
||||
|
||||
private static final String FIND_BY_ID = ""
|
||||
+ "SELECT " + USER_COLUMNS
|
||||
+ "FROM users "
|
||||
+ "WHERE id = ?";
|
||||
|
||||
private static final String FIND_BY_USERNAME = ""
|
||||
+ "SELECT " + USER_COLUMNS
|
||||
+ "FROM users "
|
||||
+ "WHERE username = ?";
|
||||
|
||||
private static final String CREATE = ""
|
||||
+ "INSERT INTO users (username, password_hash, display_name, role_code, active) "
|
||||
+ "VALUES (?, ?, ?, ?, ?)";
|
||||
|
||||
private static final String UPDATE_BASE = ""
|
||||
+ "UPDATE users "
|
||||
+ "SET display_name = ?, role_code = ?, active = ? ";
|
||||
|
||||
@Override
|
||||
public Optional<User> findActiveByUsername(String username) {
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
@@ -36,6 +64,131 @@ public class JdbcUserDao implements UserDao {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> search(UserSearchCriteria criteria) {
|
||||
UserSearchCriteria normalized = criteria == null ? new UserSearchCriteria() : criteria;
|
||||
List<Object> parameters = new ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder("SELECT ")
|
||||
.append(USER_COLUMNS)
|
||||
.append("FROM users ")
|
||||
.append("WHERE 1 = 1 ");
|
||||
|
||||
appendKeyword(sql, parameters, normalized.getKeyword());
|
||||
if (!normalized.getRoleCode().isEmpty()) {
|
||||
sql.append("AND role_code = ? ");
|
||||
parameters.add(normalized.getRoleCode());
|
||||
}
|
||||
Boolean activeValue = normalized.getActiveValue();
|
||||
if (activeValue != null) {
|
||||
sql.append("AND active = ? ");
|
||||
parameters.add(activeValue);
|
||||
}
|
||||
sql.append("ORDER BY username, id");
|
||||
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql.toString())) {
|
||||
bind(statement, parameters);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
List<User> users = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
users.add(mapUser(resultSet));
|
||||
}
|
||||
return users;
|
||||
}
|
||||
} catch (SQLException | IllegalArgumentException ex) {
|
||||
throw new DaoException("Unable to search users", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findById(long id) {
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(FIND_BY_ID)) {
|
||||
statement.setLong(1, id);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
return resultSet.next() ? Optional.of(mapUser(resultSet)) : Optional.empty();
|
||||
}
|
||||
} catch (SQLException | IllegalArgumentException ex) {
|
||||
throw new DaoException("Unable to load user by id", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findByUsername(String username) {
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(FIND_BY_USERNAME)) {
|
||||
statement.setString(1, username);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
return resultSet.next() ? Optional.of(mapUser(resultSet)) : Optional.empty();
|
||||
}
|
||||
} catch (SQLException | IllegalArgumentException ex) {
|
||||
throw new DaoException("Unable to load user by username", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long create(Connection connection, User user) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(CREATE, Statement.RETURN_GENERATED_KEYS)) {
|
||||
statement.setString(1, user.getUsername());
|
||||
statement.setString(2, user.getPasswordHash());
|
||||
statement.setString(3, user.getDisplayName());
|
||||
statement.setString(4, user.getRole().getCode());
|
||||
statement.setBoolean(5, user.isActive());
|
||||
statement.executeUpdate();
|
||||
|
||||
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
|
||||
if (generatedKeys.next()) {
|
||||
return generatedKeys.getLong(1);
|
||||
}
|
||||
}
|
||||
throw new DaoException("Unable to read generated user id", null);
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to create user", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(Connection connection, User user, boolean updatePassword) {
|
||||
String sql = UPDATE_BASE
|
||||
+ (updatePassword ? ", password_hash = ? " : "")
|
||||
+ "WHERE id = ?";
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, user.getDisplayName());
|
||||
statement.setString(2, user.getRole().getCode());
|
||||
statement.setBoolean(3, user.isActive());
|
||||
int index = 4;
|
||||
if (updatePassword) {
|
||||
statement.setString(index++, user.getPasswordHash());
|
||||
}
|
||||
statement.setLong(index, user.getId());
|
||||
return statement.executeUpdate() == 1;
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to update user", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendKeyword(StringBuilder sql, List<Object> parameters, String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String filter = "%" + value.trim() + "%";
|
||||
sql.append("AND (username LIKE ? OR display_name LIKE ?) ");
|
||||
parameters.add(filter);
|
||||
parameters.add(filter);
|
||||
}
|
||||
|
||||
private void bind(PreparedStatement statement, List<Object> parameters) throws SQLException {
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
Object value = parameters.get(i);
|
||||
if (value instanceof Boolean) {
|
||||
statement.setBoolean(i + 1, (Boolean) value);
|
||||
} else {
|
||||
statement.setString(i + 1, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private User mapUser(ResultSet resultSet) throws SQLException {
|
||||
User user = new User();
|
||||
user.setId(resultSet.getLong("id"));
|
||||
@@ -44,6 +197,12 @@ public class JdbcUserDao implements UserDao {
|
||||
user.setDisplayName(resultSet.getString("display_name"));
|
||||
user.setRole(Role.fromCode(resultSet.getString("role_code")));
|
||||
user.setActive(resultSet.getBoolean("active"));
|
||||
user.setCreatedAt(toLocalDateTime(resultSet.getTimestamp("created_at")));
|
||||
user.setUpdatedAt(toLocalDateTime(resultSet.getTimestamp("updated_at")));
|
||||
return user;
|
||||
}
|
||||
|
||||
private LocalDateTime toLocalDateTime(Timestamp timestamp) {
|
||||
return timestamp == null ? null : timestamp.toLocalDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SystemLog {
|
||||
private static final DateTimeFormatter DISPLAY_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
private long id;
|
||||
private Long operatorId;
|
||||
private String operatorUsername;
|
||||
private String operatorDisplayName;
|
||||
private String operatorRole;
|
||||
private String operationType;
|
||||
private String targetTable;
|
||||
private String targetId;
|
||||
private String resultStatus;
|
||||
private String message;
|
||||
private String requestIp;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getOperatorId() {
|
||||
return operatorId;
|
||||
}
|
||||
|
||||
public void setOperatorId(Long operatorId) {
|
||||
this.operatorId = operatorId;
|
||||
}
|
||||
|
||||
public String getOperatorUsername() {
|
||||
return operatorUsername;
|
||||
}
|
||||
|
||||
public void setOperatorUsername(String operatorUsername) {
|
||||
this.operatorUsername = operatorUsername;
|
||||
}
|
||||
|
||||
public String getOperatorDisplayName() {
|
||||
return operatorDisplayName;
|
||||
}
|
||||
|
||||
public void setOperatorDisplayName(String operatorDisplayName) {
|
||||
this.operatorDisplayName = operatorDisplayName;
|
||||
}
|
||||
|
||||
public String getOperatorRole() {
|
||||
return operatorRole;
|
||||
}
|
||||
|
||||
public void setOperatorRole(String operatorRole) {
|
||||
this.operatorRole = operatorRole;
|
||||
}
|
||||
|
||||
public String getOperationType() {
|
||||
return operationType;
|
||||
}
|
||||
|
||||
public void setOperationType(String operationType) {
|
||||
this.operationType = operationType;
|
||||
}
|
||||
|
||||
public String getTargetTable() {
|
||||
return targetTable;
|
||||
}
|
||||
|
||||
public void setTargetTable(String targetTable) {
|
||||
this.targetTable = targetTable;
|
||||
}
|
||||
|
||||
public String getTargetId() {
|
||||
return targetId;
|
||||
}
|
||||
|
||||
public void setTargetId(String targetId) {
|
||||
this.targetId = targetId;
|
||||
}
|
||||
|
||||
public String getResultStatus() {
|
||||
return resultStatus;
|
||||
}
|
||||
|
||||
public void setResultStatus(String resultStatus) {
|
||||
this.resultStatus = resultStatus;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getRequestIp() {
|
||||
return requestIp;
|
||||
}
|
||||
|
||||
public void setRequestIp(String requestIp) {
|
||||
this.requestIp = requestIp;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getCreatedAtText() {
|
||||
return createdAt == null ? "" : DISPLAY_FORMAT.format(createdAt);
|
||||
}
|
||||
|
||||
public String getOperatorLabel() {
|
||||
String displayName = trim(operatorDisplayName);
|
||||
if (!displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
String username = trim(operatorUsername);
|
||||
if (!username.isEmpty()) {
|
||||
return username;
|
||||
}
|
||||
|
||||
return operatorId == null ? "System" : "User #" + operatorId;
|
||||
}
|
||||
|
||||
public String getOperatorMetaText() {
|
||||
StringBuilder meta = new StringBuilder();
|
||||
String displayName = trim(operatorDisplayName);
|
||||
String username = trim(operatorUsername);
|
||||
if (!username.isEmpty() && !username.equals(displayName)) {
|
||||
appendMeta(meta, username);
|
||||
}
|
||||
if (operatorId != null && (!displayName.isEmpty() || !username.isEmpty())) {
|
||||
appendMeta(meta, "#" + operatorId);
|
||||
}
|
||||
appendMeta(meta, trim(operatorRole));
|
||||
return meta.toString();
|
||||
}
|
||||
|
||||
public String getResultStatusCode() {
|
||||
String normalized = trim(resultStatus).toLowerCase(Locale.ROOT);
|
||||
if ("success".equals(normalized) || "failure".equals(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
public String getResultStatusName() {
|
||||
String trimmed = trim(resultStatus);
|
||||
return trimmed.isEmpty() ? "Unknown" : trimmed;
|
||||
}
|
||||
|
||||
private void appendMeta(StringBuilder meta, String value) {
|
||||
if (value.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (meta.length() > 0) {
|
||||
meta.append(" ");
|
||||
}
|
||||
meta.append(value);
|
||||
}
|
||||
|
||||
private String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class SystemLogPage {
|
||||
private List<SystemLog> logs = Collections.emptyList();
|
||||
private List<String> operationTypes = Collections.emptyList();
|
||||
|
||||
public List<SystemLog> getLogs() {
|
||||
return logs;
|
||||
}
|
||||
|
||||
public void setLogs(List<SystemLog> logs) {
|
||||
this.logs = logs == null ? Collections.emptyList() : new ArrayList<>(logs);
|
||||
}
|
||||
|
||||
public List<String> getOperationTypes() {
|
||||
return operationTypes;
|
||||
}
|
||||
|
||||
public void setOperationTypes(List<String> operationTypes) {
|
||||
this.operationTypes = operationTypes == null ? Collections.emptyList() : new ArrayList<>(operationTypes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public class SystemLogSearchCriteria {
|
||||
private String operationType;
|
||||
private String keyword;
|
||||
private String createdFromText;
|
||||
private String createdToText;
|
||||
private LocalDate createdFrom;
|
||||
private LocalDate createdTo;
|
||||
|
||||
public SystemLogSearchCriteria() {
|
||||
this(null, null, null, null);
|
||||
}
|
||||
|
||||
public SystemLogSearchCriteria(String operationType, String keyword, String createdFromText,
|
||||
String createdToText) {
|
||||
this.operationType = trim(operationType);
|
||||
this.keyword = trim(keyword);
|
||||
this.createdFromText = trim(createdFromText);
|
||||
this.createdToText = trim(createdToText);
|
||||
}
|
||||
|
||||
public String getOperationType() {
|
||||
return operationType;
|
||||
}
|
||||
|
||||
public void setOperationType(String operationType) {
|
||||
this.operationType = trim(operationType);
|
||||
}
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = trim(keyword);
|
||||
}
|
||||
|
||||
public String getCreatedFromText() {
|
||||
return createdFromText;
|
||||
}
|
||||
|
||||
public void setCreatedFromText(String createdFromText) {
|
||||
this.createdFromText = trim(createdFromText);
|
||||
}
|
||||
|
||||
public String getCreatedToText() {
|
||||
return createdToText;
|
||||
}
|
||||
|
||||
public void setCreatedToText(String createdToText) {
|
||||
this.createdToText = trim(createdToText);
|
||||
}
|
||||
|
||||
public LocalDate getCreatedFrom() {
|
||||
return createdFrom;
|
||||
}
|
||||
|
||||
public void setCreatedFrom(LocalDate createdFrom) {
|
||||
this.createdFrom = createdFrom;
|
||||
}
|
||||
|
||||
public LocalDate getCreatedTo() {
|
||||
return createdTo;
|
||||
}
|
||||
|
||||
public void setCreatedTo(LocalDate createdTo) {
|
||||
this.createdTo = createdTo;
|
||||
}
|
||||
|
||||
private String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class User {
|
||||
private static final DateTimeFormatter DISPLAY_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
private long id;
|
||||
private String username;
|
||||
private String passwordHash;
|
||||
private String displayName;
|
||||
private Role role;
|
||||
private boolean active;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
@@ -55,4 +62,40 @@ public class User {
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public String getActiveStatusCode() {
|
||||
return active ? "active" : "inactive";
|
||||
}
|
||||
|
||||
public String getActiveStatusName() {
|
||||
return active ? "Active" : "Inactive";
|
||||
}
|
||||
|
||||
public String getCreatedAtText() {
|
||||
return format(createdAt);
|
||||
}
|
||||
|
||||
public String getUpdatedAtText() {
|
||||
return format(updatedAt);
|
||||
}
|
||||
|
||||
private String format(LocalDateTime value) {
|
||||
return value == null ? "" : DISPLAY_FORMAT.format(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
public class UserSearchCriteria {
|
||||
public static final String ACTIVE_STATUS = "active";
|
||||
public static final String INACTIVE_STATUS = "inactive";
|
||||
|
||||
private String keyword;
|
||||
private String roleCode;
|
||||
private String activeStatus;
|
||||
|
||||
public UserSearchCriteria() {
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
public UserSearchCriteria(String keyword, String roleCode, String activeStatus) {
|
||||
this.keyword = trim(keyword);
|
||||
this.roleCode = trim(roleCode);
|
||||
this.activeStatus = trim(activeStatus);
|
||||
}
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = trim(keyword);
|
||||
}
|
||||
|
||||
public String getRoleCode() {
|
||||
return roleCode;
|
||||
}
|
||||
|
||||
public void setRoleCode(String roleCode) {
|
||||
this.roleCode = trim(roleCode);
|
||||
}
|
||||
|
||||
public String getActiveStatus() {
|
||||
return activeStatus;
|
||||
}
|
||||
|
||||
public void setActiveStatus(String activeStatus) {
|
||||
this.activeStatus = trim(activeStatus);
|
||||
}
|
||||
|
||||
public Boolean getActiveValue() {
|
||||
if (ACTIVE_STATUS.equals(activeStatus)) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
if (INACTIVE_STATUS.equals(activeStatus)) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class AuthorizationFilter implements Filter {
|
||||
private static final Logger LOGGER = Logger.getLogger(AuthorizationFilter.class.getName());
|
||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||
private static final List<PathRule> RULES = Arrays.asList(
|
||||
new PathRule("/admin/system-logs", Permission.VIEW_SYSTEM_LOGS),
|
||||
new PathRule("/reports", Permission.VIEW_REPORTS),
|
||||
new PathRule("/borrowing", Permission.MANAGE_BORROWING),
|
||||
new PathRule("/books", Permission.MANAGE_BOOKS),
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.mzh.library.service;
|
||||
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.SystemLogPage;
|
||||
import com.mzh.library.entity.SystemLogSearchCriteria;
|
||||
|
||||
public interface SystemLogService {
|
||||
ServiceResult<SystemLogPage> searchLogs(AuthenticatedUser actor, SystemLogSearchCriteria criteria);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.mzh.library.service;
|
||||
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.User;
|
||||
import com.mzh.library.entity.UserSearchCriteria;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserAccountService {
|
||||
ServiceResult<List<User>> searchUsers(AuthenticatedUser actor, UserSearchCriteria criteria);
|
||||
|
||||
ServiceResult<Optional<User>> findUser(AuthenticatedUser actor, long id);
|
||||
|
||||
ServiceResult<Long> createUser(AuthenticatedUser actor, User user, String password, String requestIp);
|
||||
|
||||
ServiceResult<Void> updateUser(AuthenticatedUser actor, User user, String password, String requestIp);
|
||||
|
||||
ServiceResult<Void> deactivateUser(AuthenticatedUser actor, long id, String requestIp);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><c:out value="${formTitle}" /> - MZH Library</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<main class="page-shell">
|
||||
<section class="form-panel" aria-labelledby="user-form-title">
|
||||
<p class="eyebrow">Administration</p>
|
||||
<h1 id="user-form-title"><c:out value="${formTitle}" /></h1>
|
||||
|
||||
<c:if test="${not empty errorMessage}">
|
||||
<div class="message message-error" role="alert">
|
||||
<c:out value="${errorMessage}" />
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<c:set var="hasFormValues" value="${not empty formValues}" />
|
||||
<c:set var="usernameValue" value="${hasFormValues ? formValues.username : user.username}" />
|
||||
<c:set var="displayNameValue" value="${hasFormValues ? formValues.displayName : user.displayName}" />
|
||||
<c:set var="roleValue" value="${hasFormValues ? formValues.role : user.role.code}" />
|
||||
<c:set var="activeValue" value="${hasFormValues ? formValues.active : user.active}" />
|
||||
|
||||
<form class="user-form" action="${pageContext.request.contextPath}${formAction}" method="post" novalidate>
|
||||
<c:if test="${user.id > 0}">
|
||||
<input type="hidden" name="id" value="${user.id}">
|
||||
<input type="hidden" name="username" value="${fn:escapeXml(usernameValue)}">
|
||||
</c:if>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="username">Username</label>
|
||||
<c:choose>
|
||||
<c:when test="${user.id > 0}">
|
||||
<input id="username" type="text" value="${fn:escapeXml(usernameValue)}" disabled>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<input id="username" name="username" type="text" value="${fn:escapeXml(usernameValue)}" required>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
<c:if test="${not empty errors.username}">
|
||||
<span class="field-error"><c:out value="${errors.username}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="displayName">Display name</label>
|
||||
<input id="displayName" name="displayName" type="text"
|
||||
value="${fn:escapeXml(displayNameValue)}" required>
|
||||
<c:if test="${not empty errors.displayName}">
|
||||
<span class="field-error"><c:out value="${errors.displayName}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="role">Role</label>
|
||||
<select id="role" name="role" required>
|
||||
<option value="">Select role</option>
|
||||
<c:forEach var="role" items="${roles}">
|
||||
<option value="${role.code}" <c:if test="${roleValue == role.code}">selected</c:if>>
|
||||
<c:out value="${role.displayName}" />
|
||||
</option>
|
||||
</c:forEach>
|
||||
</select>
|
||||
<c:if test="${not empty errors.role}">
|
||||
<span class="field-error"><c:out value="${errors.role}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="active">Active state</label>
|
||||
<select id="active" name="active" required>
|
||||
<option value="true" <c:if test="${activeValue == true or activeValue == 'true'}">selected</c:if>>
|
||||
Active
|
||||
</option>
|
||||
<option value="false" <c:if test="${activeValue == false or activeValue == 'false'}">selected</c:if>>
|
||||
Inactive
|
||||
</option>
|
||||
</select>
|
||||
<c:if test="${not empty errors.active}">
|
||||
<span class="field-error"><c:out value="${errors.active}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="password">
|
||||
<c:choose>
|
||||
<c:when test="${user.id > 0}">New password</c:when>
|
||||
<c:otherwise>Password</c:otherwise>
|
||||
</c:choose>
|
||||
</label>
|
||||
<c:choose>
|
||||
<c:when test="${user.id > 0}">
|
||||
<input id="password" name="password" type="password" autocomplete="new-password">
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<input id="password" name="password" type="password" autocomplete="new-password" required>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
<c:if test="${not empty errors.password}">
|
||||
<span class="field-error"><c:out value="${errors.password}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="button button-primary" type="submit">Save</button>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,139 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Manage Users - MZH Library</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<main class="page-shell">
|
||||
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-users-title">
|
||||
<div>
|
||||
<p class="eyebrow">Administration</p>
|
||||
<h1 id="manage-users-title">Manage users</h1>
|
||||
<p>Create, update, deactivate, and review administrator, librarian, and reader accounts.</p>
|
||||
</div>
|
||||
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">New user</a>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty successMessage}">
|
||||
<div class="message message-success" role="status">
|
||||
<c:out value="${successMessage}" />
|
||||
</div>
|
||||
</c:if>
|
||||
<c:if test="${not empty errorMessage}">
|
||||
<div class="message message-error" role="alert">
|
||||
<c:out value="${errorMessage}" />
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<section class="toolbar-panel" aria-label="User management search">
|
||||
<form class="search-form" action="${pageContext.request.contextPath}/admin/users" method="get">
|
||||
<div class="search-field">
|
||||
<label for="keyword">Keyword</label>
|
||||
<input id="keyword" name="keyword" type="text" value="${fn:escapeXml(criteria.keyword)}">
|
||||
<c:if test="${not empty errors.keyword}">
|
||||
<span class="field-error"><c:out value="${errors.keyword}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="search-field">
|
||||
<label for="role">Role</label>
|
||||
<select id="role" name="role">
|
||||
<option value="">All roles</option>
|
||||
<c:forEach var="role" items="${roles}">
|
||||
<option value="${role.code}" <c:if test="${criteria.roleCode == role.code}">selected</c:if>>
|
||||
<c:out value="${role.displayName}" />
|
||||
</option>
|
||||
</c:forEach>
|
||||
</select>
|
||||
<c:if test="${not empty errors.role}">
|
||||
<span class="field-error"><c:out value="${errors.role}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="search-field">
|
||||
<label for="active">Active state</label>
|
||||
<select id="active" name="active">
|
||||
<option value="">All states</option>
|
||||
<option value="active" <c:if test="${criteria.activeStatus == 'active'}">selected</c:if>>Active</option>
|
||||
<option value="inactive" <c:if test="${criteria.activeStatus == 'inactive'}">selected</c:if>>Inactive</option>
|
||||
</select>
|
||||
<c:if test="${not empty errors.active}">
|
||||
<span class="field-error"><c:out value="${errors.active}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<button class="button button-primary" type="submit">Search</button>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Clear</a>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="table-panel" aria-labelledby="user-results-title">
|
||||
<h2 id="user-results-title">User accounts</h2>
|
||||
<c:choose>
|
||||
<c:when test="${empty users}">
|
||||
<p class="empty-state">No user accounts match the current filters.</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Username</th>
|
||||
<th scope="col">Display name</th>
|
||||
<th scope="col">Role</th>
|
||||
<th scope="col">State</th>
|
||||
<th scope="col">Created</th>
|
||||
<th scope="col">Updated</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach var="account" items="${users}">
|
||||
<tr>
|
||||
<td><c:out value="${account.username}" /></td>
|
||||
<td><c:out value="${account.displayName}" /></td>
|
||||
<td><c:out value="${account.role.displayName}" /></td>
|
||||
<td>
|
||||
<span class="status-pill status-${account.activeStatusCode}">
|
||||
<c:out value="${account.activeStatusName}" />
|
||||
</span>
|
||||
</td>
|
||||
<td><c:out value="${account.createdAtText}" /></td>
|
||||
<td><c:out value="${account.updatedAtText}" /></td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<a class="button button-secondary"
|
||||
href="${pageContext.request.contextPath}/admin/users/edit?id=${account.id}">Edit</a>
|
||||
<c:choose>
|
||||
<c:when test="${account.id == sessionScope.authenticatedUser.id or not account.active}">
|
||||
<button class="button button-secondary" type="button" disabled>Deactivate</button>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<form action="${pageContext.request.contextPath}/admin/users/deactivate"
|
||||
method="post"
|
||||
onsubmit="return confirm('Deactivate this user account?');">
|
||||
<input type="hidden" name="id" value="${account.id}">
|
||||
<button class="button button-danger" type="submit">Deactivate</button>
|
||||
</form>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,6 +7,8 @@
|
||||
<a href="${pageContext.request.contextPath}/catalog">Catalog</a>
|
||||
<c:if test="${sessionScope.userRole == 'administrator'}">
|
||||
<a href="${pageContext.request.contextPath}/admin/home">Admin</a>
|
||||
<a href="${pageContext.request.contextPath}/admin/users">Users</a>
|
||||
<a href="${pageContext.request.contextPath}/admin/system-logs">Logs</a>
|
||||
</c:if>
|
||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||
<a href="${pageContext.request.contextPath}/librarian/home">Librarian</a>
|
||||
|
||||
@@ -26,6 +26,18 @@
|
||||
<p>Account, role, permission, and system-maintenance entry point.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/home">Open</a>
|
||||
</article>
|
||||
|
||||
<article class="workspace-card">
|
||||
<h2>User Management</h2>
|
||||
<p>Create, update, deactivate, and review login accounts.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Open</a>
|
||||
</article>
|
||||
|
||||
<article class="workspace-card">
|
||||
<h2>System Logs</h2>
|
||||
<p>Review read-only audit entries for account and maintenance actions.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">Open</a>
|
||||
</article>
|
||||
</c:if>
|
||||
|
||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>System Logs - MZH Library</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<main class="page-shell">
|
||||
<section class="dashboard-hero catalog-hero" aria-labelledby="system-logs-title">
|
||||
<div>
|
||||
<p class="eyebrow">System Maintenance</p>
|
||||
<h1 id="system-logs-title">System logs</h1>
|
||||
<p>Review administrative account changes and maintenance audit records.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty errorMessage}">
|
||||
<div class="message message-error" role="alert">
|
||||
<c:out value="${errorMessage}" />
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<section class="toolbar-panel" aria-label="System log search">
|
||||
<form class="search-form system-log-search-form"
|
||||
action="${pageContext.request.contextPath}/admin/system-logs" method="get">
|
||||
<div class="search-field">
|
||||
<label for="operationType">Operation</label>
|
||||
<select id="operationType" name="operationType">
|
||||
<option value="">All operations</option>
|
||||
<c:forEach var="operationType" items="${operationTypes}">
|
||||
<option value="${fn:escapeXml(operationType)}"
|
||||
<c:if test="${criteria.operationType == operationType}">selected</c:if>>
|
||||
<c:out value="${operationType}" />
|
||||
</option>
|
||||
</c:forEach>
|
||||
<c:if test="${not empty criteria.operationType and empty operationTypes}">
|
||||
<option value="${fn:escapeXml(criteria.operationType)}" selected>
|
||||
<c:out value="${criteria.operationType}" />
|
||||
</option>
|
||||
</c:if>
|
||||
</select>
|
||||
<c:if test="${not empty errors.operationType}">
|
||||
<span class="field-error"><c:out value="${errors.operationType}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="search-field">
|
||||
<label for="keyword">Keyword</label>
|
||||
<input id="keyword" name="keyword" type="text" value="${fn:escapeXml(criteria.keyword)}">
|
||||
<c:if test="${not empty errors.keyword}">
|
||||
<span class="field-error"><c:out value="${errors.keyword}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="search-field">
|
||||
<label for="createdFrom">From</label>
|
||||
<input id="createdFrom" name="createdFrom" type="date"
|
||||
value="${fn:escapeXml(criteria.createdFromText)}">
|
||||
<c:if test="${not empty errors.createdFrom}">
|
||||
<span class="field-error"><c:out value="${errors.createdFrom}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<div class="search-field">
|
||||
<label for="createdTo">To</label>
|
||||
<input id="createdTo" name="createdTo" type="date"
|
||||
value="${fn:escapeXml(criteria.createdToText)}">
|
||||
<c:if test="${not empty errors.createdTo}">
|
||||
<span class="field-error"><c:out value="${errors.createdTo}" /></span>
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<button class="button button-primary" type="submit">Search</button>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">Clear</a>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="table-panel" aria-labelledby="system-log-results-title">
|
||||
<h2 id="system-log-results-title">Log entries</h2>
|
||||
<c:choose>
|
||||
<c:when test="${empty logs}">
|
||||
<p class="empty-state">No system logs match the current filters.</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table system-log-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Operator</th>
|
||||
<th scope="col">Operation</th>
|
||||
<th scope="col">Target</th>
|
||||
<th scope="col">Result</th>
|
||||
<th scope="col">IP address</th>
|
||||
<th scope="col">Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach var="log" items="${logs}">
|
||||
<tr>
|
||||
<td><c:out value="${log.createdAtText}" /></td>
|
||||
<td>
|
||||
<div><c:out value="${log.operatorLabel}" /></div>
|
||||
<c:if test="${not empty log.operatorMetaText}">
|
||||
<div class="muted-text"><c:out value="${log.operatorMetaText}" /></div>
|
||||
</c:if>
|
||||
</td>
|
||||
<td><c:out value="${log.operationType}" /></td>
|
||||
<td>
|
||||
<c:out value="${log.targetTable}" />
|
||||
<c:if test="${not empty log.targetId}">
|
||||
#<c:out value="${log.targetId}" />
|
||||
</c:if>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-pill status-${log.resultStatusCode}">
|
||||
<c:out value="${log.resultStatusName}" />
|
||||
</span>
|
||||
</td>
|
||||
<td><c:out value="${log.requestIp}" /></td>
|
||||
<td><c:out value="${log.message}" /></td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -28,6 +28,20 @@
|
||||
</article>
|
||||
|
||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||
<c:if test="${sessionScope.userRole == 'administrator'}">
|
||||
<article class="workspace-card">
|
||||
<h2>User Management</h2>
|
||||
<p>Create, update, deactivate, and review login accounts.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Manage users</a>
|
||||
</article>
|
||||
|
||||
<article class="workspace-card">
|
||||
<h2>System Logs</h2>
|
||||
<p>Review read-only audit entries for account and maintenance actions.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">View logs</a>
|
||||
</article>
|
||||
</c:if>
|
||||
|
||||
<article class="workspace-card">
|
||||
<h2>Book Management</h2>
|
||||
<p>Create, update, delete, and review inventory fields for book records.</p>
|
||||
|
||||
@@ -75,6 +75,28 @@
|
||||
<url-pattern>/reader/home</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>UserManagementServlet</servlet-name>
|
||||
<servlet-class>com.mzh.library.controller.UserManagementServlet</servlet-class>
|
||||
</servlet>
|
||||
<servlet-mapping>
|
||||
<servlet-name>UserManagementServlet</servlet-name>
|
||||
<url-pattern>/admin/users</url-pattern>
|
||||
<url-pattern>/admin/users/new</url-pattern>
|
||||
<url-pattern>/admin/users/edit</url-pattern>
|
||||
<url-pattern>/admin/users/update</url-pattern>
|
||||
<url-pattern>/admin/users/deactivate</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>SystemLogServlet</servlet-name>
|
||||
<servlet-class>com.mzh.library.controller.SystemLogServlet</servlet-class>
|
||||
</servlet>
|
||||
<servlet-mapping>
|
||||
<servlet-name>SystemLogServlet</servlet-name>
|
||||
<url-pattern>/admin/system-logs</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>BookCatalogServlet</servlet-name>
|
||||
<servlet-class>com.mzh.library.controller.BookCatalogServlet</servlet-class>
|
||||
|
||||
@@ -198,6 +198,11 @@ h2 {
|
||||
background: #8f3028;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
@@ -326,6 +331,10 @@ h2 {
|
||||
grid-template-columns: repeat(3, minmax(120px, 1fr)) auto auto;
|
||||
}
|
||||
|
||||
.system-log-search-form {
|
||||
grid-template-columns: repeat(4, minmax(120px, 1fr)) auto auto;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -343,6 +352,8 @@ h2 {
|
||||
.book-form select,
|
||||
.reader-form input,
|
||||
.reader-form select,
|
||||
.user-form input,
|
||||
.user-form select,
|
||||
.borrow-form input {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
@@ -359,6 +370,8 @@ h2 {
|
||||
.book-form select:focus,
|
||||
.reader-form input:focus,
|
||||
.reader-form select:focus,
|
||||
.user-form input:focus,
|
||||
.user-form select:focus,
|
||||
.borrow-form input:focus {
|
||||
outline: 3px solid rgba(37, 111, 108, 0.18);
|
||||
border-color: var(--color-primary);
|
||||
@@ -380,6 +393,11 @@ h2 {
|
||||
min-width: 980px;
|
||||
}
|
||||
|
||||
.user-table,
|
||||
.system-log-table {
|
||||
min-width: 980px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 12px 10px;
|
||||
@@ -445,6 +463,21 @@ h2 {
|
||||
background: #eef1f5;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--color-success);
|
||||
background: #edf8ef;
|
||||
}
|
||||
|
||||
.status-failure {
|
||||
color: #7a211a;
|
||||
background: #fff0ee;
|
||||
}
|
||||
|
||||
.status-unknown {
|
||||
color: var(--color-muted);
|
||||
background: #eef1f5;
|
||||
}
|
||||
|
||||
.status-overdue {
|
||||
color: #7a211a;
|
||||
background: #fff0ee;
|
||||
@@ -472,6 +505,7 @@ h2 {
|
||||
|
||||
.book-form,
|
||||
.reader-form,
|
||||
.user-form,
|
||||
.borrow-form {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
|
||||
Reference in New Issue
Block a user