mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
feat: add audit log system for tracking admin-significant actions (#2759)
This commit is contained in:
@@ -26,6 +26,8 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@@ -39,6 +41,7 @@ public class AuthenticationService {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtUtils jwtUtils;
|
||||
private final DefaultSettingInitializer defaultSettingInitializer;
|
||||
private final AuditService auditService;
|
||||
|
||||
public BookLoreUser getAuthenticatedUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
@@ -87,9 +90,13 @@ public class AuthenticationService {
|
||||
}
|
||||
|
||||
public ResponseEntity<Map<String, String>> loginUser(UserLoginRequest loginRequest) {
|
||||
BookLoreUserEntity user = userRepository.findByUsername(loginRequest.getUsername()).orElseThrow(() -> ApiError.USER_NOT_FOUND.createException(loginRequest.getUsername()));
|
||||
BookLoreUserEntity user = userRepository.findByUsername(loginRequest.getUsername()).orElseThrow(() -> {
|
||||
auditService.log(AuditAction.LOGIN_FAILED, "Login failed for unknown user: " + loginRequest.getUsername());
|
||||
return ApiError.USER_NOT_FOUND.createException(loginRequest.getUsername());
|
||||
});
|
||||
|
||||
if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPasswordHash())) {
|
||||
auditService.log(AuditAction.LOGIN_FAILED, "Login failed for user: " + loginRequest.getUsername());
|
||||
throw ApiError.INVALID_CREDENTIALS.createException();
|
||||
}
|
||||
|
||||
@@ -125,6 +132,7 @@ public class AuthenticationService {
|
||||
.build();
|
||||
|
||||
refreshTokenRepository.save(refreshTokenEntity);
|
||||
auditService.log(AuditAction.LOGIN_SUCCESS, "User", user.getId(), "Login successful for user: " + user.getUsername());
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"accessToken", accessToken,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.booklore.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.booklore.model.dto.response.AuditLogDto;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/audit-logs")
|
||||
@Tag(name = "Audit Logs", description = "Endpoints for viewing audit logs")
|
||||
public class AuditLogController {
|
||||
|
||||
private final AuditService auditService;
|
||||
|
||||
@Operation(summary = "Get audit logs", description = "Retrieve paginated audit logs with optional filters. Requires admin.")
|
||||
@ApiResponse(responseCode = "200", description = "Audit logs returned successfully")
|
||||
@GetMapping
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
public ResponseEntity<Page<AuditLogDto>> getAuditLogs(
|
||||
@Parameter(description = "Page number") @RequestParam(defaultValue = "0") int page,
|
||||
@Parameter(description = "Page size") @RequestParam(defaultValue = "25") int size,
|
||||
@Parameter(description = "Filter by action") @RequestParam(required = false) AuditAction action,
|
||||
@Parameter(description = "Filter by user ID") @RequestParam(required = false) Long userId,
|
||||
@Parameter(description = "Filter by username") @RequestParam(required = false) String username,
|
||||
@Parameter(description = "Filter from date") @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime from,
|
||||
@Parameter(description = "Filter to date") @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to) {
|
||||
|
||||
Page<AuditLogDto> logs;
|
||||
if (action == null && userId == null && username == null && from == null && to == null) {
|
||||
logs = auditService.getAuditLogs(PageRequest.of(page, size));
|
||||
} else {
|
||||
logs = auditService.getAuditLogs(action, userId, username, from, to, PageRequest.of(page, size));
|
||||
}
|
||||
return ResponseEntity.ok(logs);
|
||||
}
|
||||
|
||||
@Operation(summary = "Get distinct usernames", description = "Retrieve distinct usernames from audit logs for filtering.")
|
||||
@GetMapping("/usernames")
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
public ResponseEntity<List<String>> getDistinctUsernames() {
|
||||
return ResponseEntity.ok(auditService.getDistinctUsernames());
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/books")
|
||||
@@ -41,6 +43,7 @@ public class MetadataController {
|
||||
private final MetadataMatchService metadataMatchService;
|
||||
private final BookRepository bookRepository;
|
||||
private final MetadataManagementService metadataManagementService;
|
||||
private final AuditService auditService;
|
||||
|
||||
@Operation(summary = "Get prospective metadata for a book", description = "Fetch prospective metadata for a book by its ID. Requires metadata edit permission or admin.")
|
||||
@ApiResponse(responseCode = "200", description = "Prospective metadata returned successfully")
|
||||
@@ -78,6 +81,7 @@ public class MetadataController {
|
||||
|
||||
bookMetadataUpdater.setBookMetadata(context);
|
||||
bookRepository.save(bookEntity);
|
||||
auditService.log(AuditAction.METADATA_UPDATED, "Book", bookId, "Updated metadata for book: " + bookEntity.getMetadata().getTitle());
|
||||
BookMetadata bookMetadata = bookMetadataMapper.toBookMetadata(bookEntity.getMetadata(), true);
|
||||
return ResponseEntity.ok(bookMetadata);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.booklore.model.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AuditLogDto {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String username;
|
||||
private AuditAction action;
|
||||
private String entityType;
|
||||
private Long entityId;
|
||||
private String description;
|
||||
private String ipAddress;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.booklore.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "audit_log")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AuditLogEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "username", nullable = false)
|
||||
private String username;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "action", nullable = false, length = 100)
|
||||
private AuditAction action;
|
||||
|
||||
@Column(name = "entity_type", length = 100)
|
||||
private String entityType;
|
||||
|
||||
@Column(name = "entity_id")
|
||||
private Long entityId;
|
||||
|
||||
@Column(name = "description", nullable = false, length = 1024)
|
||||
private String description;
|
||||
|
||||
@Column(name = "ip_address", length = 45)
|
||||
private String ipAddress;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.booklore.model.enums;
|
||||
|
||||
public enum AuditAction {
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAILED,
|
||||
USER_CREATED,
|
||||
USER_UPDATED,
|
||||
USER_DELETED,
|
||||
PASSWORD_CHANGED,
|
||||
PERMISSIONS_CHANGED,
|
||||
LIBRARY_CREATED,
|
||||
LIBRARY_UPDATED,
|
||||
LIBRARY_DELETED,
|
||||
LIBRARY_SCANNED,
|
||||
BOOK_UPLOADED,
|
||||
BOOK_DELETED,
|
||||
BOOK_SENT,
|
||||
METADATA_UPDATED,
|
||||
SETTINGS_UPDATED,
|
||||
OIDC_CONFIG_CHANGED,
|
||||
TASK_EXECUTED,
|
||||
SHELF_CREATED,
|
||||
SHELF_UPDATED,
|
||||
SHELF_DELETED,
|
||||
MAGIC_SHELF_CREATED,
|
||||
MAGIC_SHELF_UPDATED,
|
||||
MAGIC_SHELF_DELETED,
|
||||
EMAIL_PROVIDER_CREATED,
|
||||
EMAIL_PROVIDER_UPDATED,
|
||||
EMAIL_PROVIDER_DELETED,
|
||||
OPDS_USER_CREATED,
|
||||
OPDS_USER_DELETED,
|
||||
OPDS_USER_UPDATED,
|
||||
NAMING_PATTERN_CHANGED
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.booklore.repository;
|
||||
|
||||
import org.booklore.model.entity.AuditLogEntity;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface AuditLogRepository extends JpaRepository<AuditLogEntity, Long> {
|
||||
|
||||
Page<AuditLogEntity> findAllByOrderByCreatedAtDesc(Pageable pageable);
|
||||
|
||||
@Query("""
|
||||
SELECT a FROM AuditLogEntity a
|
||||
WHERE (:action IS NULL OR a.action = :action)
|
||||
AND (:userId IS NULL OR a.userId = :userId)
|
||||
AND (:username IS NULL OR a.username = :username)
|
||||
AND (:from IS NULL OR a.createdAt >= :from)
|
||||
AND (:to IS NULL OR a.createdAt <= :to)
|
||||
ORDER BY a.createdAt DESC
|
||||
""")
|
||||
Page<AuditLogEntity> findFiltered(
|
||||
@Param("action") AuditAction action,
|
||||
@Param("userId") Long userId,
|
||||
@Param("username") String username,
|
||||
@Param("from") LocalDateTime from,
|
||||
@Param("to") LocalDateTime to,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("SELECT DISTINCT a.username FROM AuditLogEntity a ORDER BY a.username")
|
||||
List<String> findDistinctUsernames();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.booklore.repository;
|
||||
|
||||
import org.booklore.model.entity.AuthorEntity;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -158,7 +159,7 @@ public interface BookOpdsRepository extends JpaRepository<BookEntity, Long>, Jpa
|
||||
WHERE (b.deleted IS NULL OR b.deleted = false)
|
||||
ORDER BY a.name
|
||||
""")
|
||||
List<org.booklore.model.entity.AuthorEntity> findDistinctAuthors();
|
||||
List<AuthorEntity> findDistinctAuthors();
|
||||
|
||||
@Query("""
|
||||
SELECT DISTINCT a FROM AuthorEntity a
|
||||
@@ -168,7 +169,7 @@ public interface BookOpdsRepository extends JpaRepository<BookEntity, Long>, Jpa
|
||||
AND b.library.id IN :libraryIds
|
||||
ORDER BY a.name
|
||||
""")
|
||||
List<org.booklore.model.entity.AuthorEntity> findDistinctAuthorsByLibraryIds(@Param("libraryIds") Collection<Long> libraryIds);
|
||||
List<AuthorEntity> findDistinctAuthorsByLibraryIds(@Param("libraryIds") Collection<Long> libraryIds);
|
||||
|
||||
// ============================================
|
||||
// BOOKS BY AUTHOR - Two Query Pattern
|
||||
|
||||
@@ -5,6 +5,8 @@ import org.booklore.model.dto.MagicShelf;
|
||||
import org.booklore.model.entity.MagicShelfEntity;
|
||||
import org.booklore.repository.MagicShelfRepository;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -17,6 +19,7 @@ public class MagicShelfService {
|
||||
|
||||
private final MagicShelfRepository magicShelfRepository;
|
||||
private final AuthenticationService authenticationService;
|
||||
private final AuditService auditService;
|
||||
|
||||
public List<MagicShelf> getUserShelves() {
|
||||
Long userId = authenticationService.getAuthenticatedUser().getId();
|
||||
@@ -59,12 +62,16 @@ public class MagicShelfService {
|
||||
existing.setIconType(dto.getIconType());
|
||||
existing.setFilterJson(dto.getFilterJson());
|
||||
existing.setPublic(dto.getIsPublic());
|
||||
return toDto(magicShelfRepository.save(existing));
|
||||
MagicShelf result = toDto(magicShelfRepository.save(existing));
|
||||
auditService.log(AuditAction.MAGIC_SHELF_UPDATED, "MagicShelf", dto.getId(), "Updated magic shelf: " + dto.getName());
|
||||
return result;
|
||||
}
|
||||
if (magicShelfRepository.existsByUserIdAndName(userId, dto.getName())) {
|
||||
throw new IllegalArgumentException("A shelf with the same name already exists for this user.");
|
||||
}
|
||||
return toDto(magicShelfRepository.save(toEntity(dto, userId)));
|
||||
MagicShelf result = toDto(magicShelfRepository.save(toEntity(dto, userId)));
|
||||
auditService.log(AuditAction.MAGIC_SHELF_CREATED, "MagicShelf", result.getId(), "Created magic shelf: " + dto.getName());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -74,7 +81,9 @@ public class MagicShelfService {
|
||||
if (!shelf.getUserId().equals(userId)) {
|
||||
throw new SecurityException("You are not authorized to delete this shelf");
|
||||
}
|
||||
String shelfName = shelf.getName();
|
||||
magicShelfRepository.deleteById(id);
|
||||
auditService.log(AuditAction.MAGIC_SHELF_DELETED, "MagicShelf", id, "Deleted magic shelf: " + shelfName);
|
||||
}
|
||||
|
||||
private MagicShelf toDto(MagicShelfEntity entity) {
|
||||
|
||||
@@ -15,6 +15,8 @@ import org.booklore.repository.BookRepository;
|
||||
import org.booklore.repository.ShelfRepository;
|
||||
import org.booklore.repository.UserRepository;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -31,6 +33,7 @@ public class ShelfService {
|
||||
private final BookMapper bookMapper;
|
||||
private final AuthenticationService authenticationService;
|
||||
private final UserRepository userRepository;
|
||||
private final AuditService auditService;
|
||||
|
||||
public Shelf createShelf(ShelfCreateRequest request) {
|
||||
Long userId = getAuthenticatedUserId();
|
||||
@@ -47,7 +50,9 @@ public class ShelfService {
|
||||
.isPublic(request.isPublicShelf())
|
||||
.user(fetchUserEntityById(userId))
|
||||
.build();
|
||||
return shelfMapper.toShelf(shelfRepository.save(shelfEntity));
|
||||
Shelf result = shelfMapper.toShelf(shelfRepository.save(shelfEntity));
|
||||
auditService.log(AuditAction.SHELF_CREATED, "Shelf", shelfEntity.getId(), "Created shelf: " + request.getName());
|
||||
return result;
|
||||
}
|
||||
|
||||
public Shelf updateShelf(Long id, ShelfCreateRequest request) {
|
||||
@@ -59,7 +64,9 @@ public class ShelfService {
|
||||
shelfEntity.setIcon(request.getIcon());
|
||||
shelfEntity.setIconType(request.getIconType());
|
||||
shelfEntity.setPublic(request.isPublicShelf());
|
||||
return shelfMapper.toShelf(shelfRepository.save(shelfEntity));
|
||||
Shelf result = shelfMapper.toShelf(shelfRepository.save(shelfEntity));
|
||||
auditService.log(AuditAction.SHELF_UPDATED, "Shelf", id, "Updated shelf: " + request.getName());
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Shelf> getShelves() {
|
||||
@@ -75,6 +82,7 @@ public class ShelfService {
|
||||
|
||||
public void deleteShelf(Long shelfId) {
|
||||
shelfRepository.deleteById(shelfId);
|
||||
auditService.log(AuditAction.SHELF_DELETED, "Shelf", shelfId, "Deleted shelf: " + shelfId);
|
||||
}
|
||||
|
||||
public Shelf getUserKoboShelf() {
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@Service
|
||||
@DependsOnDatabaseInitialization
|
||||
@@ -28,14 +30,16 @@ public class AppSettingService {
|
||||
private final AppProperties appProperties;
|
||||
private final SettingPersistenceHelper settingPersistenceHelper;
|
||||
private final AuthenticationService authenticationService;
|
||||
private final AuditService auditService;
|
||||
|
||||
private volatile AppSettings appSettings;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public AppSettingService(AppProperties appProperties, SettingPersistenceHelper settingPersistenceHelper, @Lazy AuthenticationService authenticationService) {
|
||||
public AppSettingService(AppProperties appProperties, SettingPersistenceHelper settingPersistenceHelper, @Lazy AuthenticationService authenticationService, @Lazy AuditService auditService) {
|
||||
this.appProperties = appProperties;
|
||||
this.settingPersistenceHelper = settingPersistenceHelper;
|
||||
this.authenticationService = authenticationService;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
public AppSettings getAppSettings() {
|
||||
@@ -66,6 +70,8 @@ public class AppSettingService {
|
||||
setting.setVal(settingPersistenceHelper.serializeSettingValue(key, val));
|
||||
settingPersistenceHelper.appSettingsRepository.save(setting);
|
||||
refreshCache();
|
||||
AuditAction action = key.name().startsWith("OIDC_") ? AuditAction.OIDC_CONFIG_CHANGED : AuditAction.SETTINGS_UPDATED;
|
||||
auditService.log(action, "Updated setting: " + key);
|
||||
}
|
||||
|
||||
private void validatePermission(AppSettingKey key, BookLoreUser user) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.booklore.service.audit;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.response.AuditLogDto;
|
||||
import org.booklore.model.entity.AuditLogEntity;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.repository.AuditLogRepository;
|
||||
import org.booklore.util.RequestUtils;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuditService {
|
||||
|
||||
private final AuditLogRepository auditLogRepository;
|
||||
|
||||
public void log(AuditAction action, String description) {
|
||||
log(action, null, null, description);
|
||||
}
|
||||
|
||||
public void log(AuditAction action, String entityType, Long entityId, String description) {
|
||||
try {
|
||||
Long userId = null;
|
||||
String username = "system";
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.getPrincipal() instanceof BookLoreUser user) {
|
||||
userId = user.getId();
|
||||
username = user.getUsername();
|
||||
}
|
||||
|
||||
String ipAddress = null;
|
||||
try {
|
||||
ipAddress = RequestUtils.getCurrentRequest().getRemoteAddr();
|
||||
} catch (Exception ignored) {
|
||||
// Non-HTTP context (scheduled tasks, etc.)
|
||||
}
|
||||
|
||||
AuditLogEntity entity = AuditLogEntity.builder()
|
||||
.userId(userId)
|
||||
.username(username)
|
||||
.action(action)
|
||||
.entityType(entityType)
|
||||
.entityId(entityId)
|
||||
.description(description)
|
||||
.ipAddress(ipAddress)
|
||||
.build();
|
||||
|
||||
auditLogRepository.save(entity);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to write audit log: action={}, description={}", action, description, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Page<AuditLogDto> getAuditLogs(Pageable pageable) {
|
||||
return auditLogRepository.findAllByOrderByCreatedAtDesc(pageable)
|
||||
.map(this::toDto);
|
||||
}
|
||||
|
||||
public Page<AuditLogDto> getAuditLogs(AuditAction action, Long userId, String username, LocalDateTime from, LocalDateTime to, Pageable pageable) {
|
||||
return auditLogRepository.findFiltered(action, userId, username, from, to, pageable)
|
||||
.map(this::toDto);
|
||||
}
|
||||
|
||||
public List<String> getDistinctUsernames() {
|
||||
return auditLogRepository.findDistinctUsernames();
|
||||
}
|
||||
|
||||
private AuditLogDto toDto(AuditLogEntity entity) {
|
||||
return AuditLogDto.builder()
|
||||
.id(entity.getId())
|
||||
.userId(entity.getUserId())
|
||||
.username(entity.getUsername())
|
||||
.action(entity.getAction())
|
||||
.entityType(entity.getEntityType())
|
||||
.entityId(entity.getEntityId())
|
||||
.description(entity.getDescription())
|
||||
.ipAddress(entity.getIpAddress())
|
||||
.createdAt(entity.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,8 @@ import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@@ -69,6 +71,7 @@ public class BookService {
|
||||
private final EbookViewerPreferenceRepository ebookViewerPreferencesRepository;
|
||||
private final SidecarMetadataWriter sidecarMetadataWriter;
|
||||
private final FileStreamingService fileStreamingService;
|
||||
private final AuditService auditService;
|
||||
|
||||
|
||||
public List<Book> getBookDTOs(boolean includeDescription) {
|
||||
@@ -513,6 +516,7 @@ public class BookService {
|
||||
}
|
||||
|
||||
bookRepository.deleteAll(books);
|
||||
auditService.log(AuditAction.BOOK_DELETED, "Deleted " + ids.size() + " book(s)");
|
||||
BookDeletionResponse response = new BookDeletionResponse(ids, failedFileDeletions);
|
||||
return failedFileDeletions.isEmpty()
|
||||
? ResponseEntity.ok(response)
|
||||
|
||||
@@ -13,6 +13,8 @@ import org.booklore.repository.UserEmailProviderPreferenceRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
@@ -27,6 +29,7 @@ public class EmailProviderV2Service {
|
||||
private final UserEmailProviderPreferenceRepository preferenceRepository;
|
||||
private final EmailProviderV2Mapper mapper;
|
||||
private final AuthenticationService authService;
|
||||
private final AuditService auditService;
|
||||
|
||||
public List<EmailProviderV2> getEmailProviders() {
|
||||
BookLoreUser user = authService.getAuthenticatedUser();
|
||||
@@ -64,6 +67,7 @@ public class EmailProviderV2Service {
|
||||
}
|
||||
|
||||
Long defaultProviderId = getDefaultProviderIdForUser(user.getId());
|
||||
auditService.log(AuditAction.EMAIL_PROVIDER_CREATED, "EmailProvider", savedEntity.getId(), "Created email provider: " + savedEntity.getHost() + ":" + savedEntity.getPort());
|
||||
return mapper.toDTO(savedEntity, defaultProviderId);
|
||||
}
|
||||
|
||||
@@ -78,6 +82,7 @@ public class EmailProviderV2Service {
|
||||
existingProvider.setShared(request.isShared());
|
||||
}
|
||||
EmailProviderV2Entity updatedEntity = repository.save(existingProvider);
|
||||
auditService.log(AuditAction.EMAIL_PROVIDER_UPDATED, "EmailProvider", id, "Updated email provider: " + updatedEntity.getHost() + ":" + updatedEntity.getPort());
|
||||
|
||||
Long defaultProviderId = getDefaultProviderIdForUser(user.getId());
|
||||
return mapper.toDTO(updatedEntity, defaultProviderId);
|
||||
@@ -118,6 +123,7 @@ public class EmailProviderV2Service {
|
||||
}
|
||||
|
||||
repository.deleteById(id);
|
||||
auditService.log(AuditAction.EMAIL_PROVIDER_DELETED, "EmailProvider", id, "Deleted email provider");
|
||||
}
|
||||
|
||||
private Long getDefaultProviderIdForUser(Long userId) {
|
||||
|
||||
@@ -23,6 +23,8 @@ import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -41,6 +43,7 @@ public class SendEmailV2Service {
|
||||
private final EmailRecipientV2Repository emailRecipientRepository;
|
||||
private final NotificationService notificationService;
|
||||
private final AuthenticationService authenticationService;
|
||||
private final AuditService auditService;
|
||||
|
||||
public void emailBookQuick(Long bookId) {
|
||||
BookLoreUser user = authenticationService.getAuthenticatedUser();
|
||||
@@ -72,6 +75,7 @@ public class SendEmailV2Service {
|
||||
SecurityContextVirtualThread.runWithSecurityContext(() -> {
|
||||
try {
|
||||
sendEmail(emailProvider, recipientEmail, book, bookFile);
|
||||
auditService.log(AuditAction.BOOK_SENT, "Book", book.getId(), "Sent book: " + bookTitle + " to " + recipientEmail);
|
||||
String successMessage = "The book: " + bookTitle + " has been successfully sent to " + recipientEmail;
|
||||
notificationService.sendMessage(Topic.LOG, LogNotification.info(successMessage));
|
||||
log.info(successMessage);
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.LibraryEntity;
|
||||
import org.booklore.model.entity.LibraryPathEntity;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.model.enums.BookFileType;
|
||||
import org.booklore.model.websocket.Topic;
|
||||
import org.booklore.repository.BookRepository;
|
||||
@@ -42,6 +43,7 @@ import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -71,6 +73,7 @@ public class LibraryService {
|
||||
private final MonitoringService monitoringService;
|
||||
private final AuthenticationService authenticationService;
|
||||
private final UserRepository userRepository;
|
||||
private final AuditService auditService;
|
||||
|
||||
@Transactional
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@@ -158,6 +161,7 @@ public class LibraryService {
|
||||
});
|
||||
}
|
||||
|
||||
auditService.log(AuditAction.LIBRARY_UPDATED, "Library", libraryId, "Updated library: " + library.getName());
|
||||
return libraryMapper.toLibrary(savedLibrary);
|
||||
}
|
||||
|
||||
@@ -208,11 +212,13 @@ public class LibraryService {
|
||||
log.info("Parsing task completed!");
|
||||
});
|
||||
|
||||
auditService.log(AuditAction.LIBRARY_CREATED, "Library", libraryEntity.getId(), "Created library: " + libraryEntity.getName());
|
||||
return libraryMapper.toLibrary(libraryEntity);
|
||||
}
|
||||
|
||||
public void rescanLibrary(long libraryId) {
|
||||
libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
|
||||
LibraryEntity lib = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
|
||||
auditService.log(AuditAction.LIBRARY_SCANNED, "Library", libraryId, "Scanned library: " + lib.getName());
|
||||
|
||||
SecurityContextVirtualThread.runWithSecurityContext(() -> {
|
||||
if (!scanningLibraries.add(libraryId)) {
|
||||
@@ -265,7 +271,9 @@ public class LibraryService {
|
||||
monitoringService.unregisterLibrary(id);
|
||||
Set<Long> bookIds = library.getBookEntities().stream().map(BookEntity::getId).collect(Collectors.toSet());
|
||||
fileService.deleteBookCovers(bookIds);
|
||||
String libraryName = library.getName();
|
||||
libraryRepository.deleteById(id);
|
||||
auditService.log(AuditAction.LIBRARY_DELETED, "Library", id, "Deleted library: " + libraryName);
|
||||
log.info("Library deleted successfully: {}", id);
|
||||
}
|
||||
|
||||
@@ -284,7 +292,9 @@ public class LibraryService {
|
||||
public Library setFileNamingPattern(long libraryId, String pattern) {
|
||||
LibraryEntity library = libraryRepository.findById(libraryId).orElseThrow(() -> ApiError.LIBRARY_NOT_FOUND.createException(libraryId));
|
||||
library.setFileNamingPattern(pattern);
|
||||
return libraryMapper.toLibrary(libraryRepository.save(library));
|
||||
Library result = libraryMapper.toLibrary(libraryRepository.save(library));
|
||||
auditService.log(AuditAction.NAMING_PATTERN_CHANGED, "Library", libraryId, "Changed naming pattern for library: " + library.getName() + " to: " + pattern);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Long> getBookCountsByFormat(long libraryId) {
|
||||
|
||||
@@ -367,7 +367,7 @@ public class EpubMetadataWriter implements MetadataWriter {
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentBuilder builder = org.booklore.util.SecureXmlUtils.createSecureDocumentBuilder(true);
|
||||
DocumentBuilder builder = SecureXmlUtils.createSecureDocumentBuilder(true);
|
||||
Document opfDoc = builder.parse(opfFile);
|
||||
|
||||
applyCoverImageToEpub(tempDir, opfDoc, coverData);
|
||||
@@ -486,7 +486,7 @@ public class EpubMetadataWriter implements MetadataWriter {
|
||||
throw new IOException("container.xml not found at expected location: " + containerXml);
|
||||
}
|
||||
|
||||
DocumentBuilder builder = org.booklore.util.SecureXmlUtils.createSecureDocumentBuilder(false);
|
||||
DocumentBuilder builder = SecureXmlUtils.createSecureDocumentBuilder(false);
|
||||
Document containerDoc = builder.parse(containerXml.toFile());
|
||||
Node rootfile = containerDoc.getElementsByTagName("rootfile").item(0);
|
||||
if (rootfile == null) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.booklore.model.dto.settings.MetadataPersistenceSettings;
|
||||
import org.booklore.model.entity.BookMetadataEntity;
|
||||
import org.booklore.model.enums.BookFileType;
|
||||
import org.booklore.service.appsettings.AppSettingService;
|
||||
import org.booklore.util.SecureXmlUtils;
|
||||
import org.booklore.service.metadata.BookLoreMetadata;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.w3c.dom.Document;
|
||||
@@ -247,7 +248,7 @@ public class PdfMetadataWriter implements MetadataWriter {
|
||||
* - Booklore (booklore:) for series, subtitle, ISBNs, external IDs, ratings, moods, tags, page count
|
||||
*/
|
||||
private byte[] addCustomIdentifiersToXmp(byte[] xmpBytes, BookMetadataEntity metadata, MetadataCopyHelper helper, MetadataClearFlags clear) throws Exception {
|
||||
DocumentBuilder builder = org.booklore.util.SecureXmlUtils.createSecureDocumentBuilder(true);
|
||||
DocumentBuilder builder = SecureXmlUtils.createSecureDocumentBuilder(true);
|
||||
Document doc = builder.parse(new ByteArrayInputStream(xmpBytes));
|
||||
|
||||
Element rdfRoot = (Element) doc.getElementsByTagNameNS("http://www.w3.org/1999/02/22-rdf-syntax-ns#", "RDF").item(0);
|
||||
@@ -442,7 +443,7 @@ public class PdfMetadataWriter implements MetadataWriter {
|
||||
private boolean isXmpMetadataDifferent(byte[] existingBytes, byte[] newBytes) {
|
||||
if (existingBytes == null || newBytes == null) return true;
|
||||
try {
|
||||
DocumentBuilder builder = org.booklore.util.SecureXmlUtils.createSecureDocumentBuilder(false);
|
||||
DocumentBuilder builder = SecureXmlUtils.createSecureDocumentBuilder(false);
|
||||
Document doc1 = builder.parse(new ByteArrayInputStream(existingBytes));
|
||||
Document doc2 = builder.parse(new ByteArrayInputStream(newBytes));
|
||||
return !Objects.equals(
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.booklore.exception.ApiError;
|
||||
import org.booklore.mapper.BookMapper;
|
||||
import org.booklore.mapper.custom.BookLoreUserTransformer;
|
||||
import org.booklore.model.dto.*;
|
||||
import org.booklore.model.entity.AuthorEntity;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.ShelfEntity;
|
||||
@@ -171,7 +172,7 @@ public class OpdsBookService {
|
||||
.orElseThrow(() -> ApiError.USER_NOT_FOUND.createException(userId));
|
||||
BookLoreUser user = bookLoreUserTransformer.toDTO(entity);
|
||||
|
||||
List<org.booklore.model.entity.AuthorEntity> authors;
|
||||
List<AuthorEntity> authors;
|
||||
|
||||
if (user.getPermissions().isAdmin()) {
|
||||
authors = bookOpdsRepository.findDistinctAuthors();
|
||||
@@ -183,7 +184,7 @@ public class OpdsBookService {
|
||||
}
|
||||
|
||||
return authors.stream()
|
||||
.map(org.booklore.model.entity.AuthorEntity::getName)
|
||||
.map(AuthorEntity::getName)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.sorted()
|
||||
|
||||
@@ -8,9 +8,12 @@ import org.booklore.model.dto.request.OpdsUserV2CreateRequest;
|
||||
import org.booklore.model.dto.request.OpdsUserV2UpdateRequest;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.OpdsUserV2Entity;
|
||||
import org.booklore.model.enums.OpdsSortOrder;
|
||||
import org.booklore.repository.OpdsUserV2Repository;
|
||||
import org.booklore.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
@@ -28,6 +31,7 @@ public class OpdsUserV2Service {
|
||||
private final UserRepository userRepository;
|
||||
private final OpdsUserV2Mapper mapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final AuditService auditService;
|
||||
|
||||
|
||||
public List<OpdsUserV2> getOpdsUsers() {
|
||||
@@ -46,10 +50,12 @@ public class OpdsUserV2Service {
|
||||
.user(userEntity)
|
||||
.username(request.getUsername())
|
||||
.passwordHash(passwordEncoder.encode(request.getPassword()))
|
||||
.sortOrder(request.getSortOrder() != null ? request.getSortOrder() : org.booklore.model.enums.OpdsSortOrder.RECENT)
|
||||
.sortOrder(request.getSortOrder() != null ? request.getSortOrder() : OpdsSortOrder.RECENT)
|
||||
.build();
|
||||
|
||||
return mapper.toDto(opdsUserV2Repository.save(opdsUserV2));
|
||||
OpdsUserV2 result = mapper.toDto(opdsUserV2Repository.save(opdsUserV2));
|
||||
auditService.log(AuditAction.OPDS_USER_CREATED, "OpdsUser", result.getId(), "Created OPDS user: " + request.getUsername());
|
||||
return result;
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
if (e.getMostSpecificCause().getMessage().contains("uq_username")) {
|
||||
throw new DataIntegrityViolationException("Username '" + request.getUsername() + "' is already taken");
|
||||
@@ -64,7 +70,9 @@ public class OpdsUserV2Service {
|
||||
if (!user.getUser().getId().equals(bookLoreUser.getId())) {
|
||||
throw new AccessDeniedException("You are not allowed to delete this user");
|
||||
}
|
||||
String username = user.getUsername();
|
||||
opdsUserV2Repository.delete(user);
|
||||
auditService.log(AuditAction.OPDS_USER_DELETED, "OpdsUser", userId, "Deleted OPDS user: " + username);
|
||||
}
|
||||
|
||||
public OpdsUserV2 updateOpdsUser(Long userId, OpdsUserV2UpdateRequest request) {
|
||||
@@ -77,7 +85,9 @@ public class OpdsUserV2Service {
|
||||
}
|
||||
|
||||
user.setSortOrder(request.sortOrder());
|
||||
return mapper.toDto(opdsUserV2Repository.save(user));
|
||||
OpdsUserV2 result = mapper.toDto(opdsUserV2Repository.save(user));
|
||||
auditService.log(AuditAction.OPDS_USER_UPDATED, "OpdsUser", userId, "Updated OPDS user: " + user.getUsername());
|
||||
return result;
|
||||
}
|
||||
|
||||
public OpdsUserV2Entity findByUsername(String username) {
|
||||
|
||||
@@ -15,6 +15,8 @@ import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -22,6 +24,7 @@ import java.util.stream.Collectors;
|
||||
public class TaskHistoryService {
|
||||
|
||||
private final TaskHistoryRepository taskHistoryRepository;
|
||||
private final AuditService auditService;
|
||||
|
||||
@Transactional
|
||||
public void createTask(String taskId, TaskType type, Long userId, Map<String, Object> options) {
|
||||
@@ -35,6 +38,23 @@ public class TaskHistoryService {
|
||||
.taskOptions(options)
|
||||
.build();
|
||||
taskHistoryRepository.save(task);
|
||||
auditService.log(AuditAction.TASK_EXECUTED, "Task", null, buildTaskDescription(type, options));
|
||||
}
|
||||
|
||||
private String buildTaskDescription(TaskType type, Map<String, Object> options) {
|
||||
String taskName = type != null ? type.getName() : "Unknown";
|
||||
StringBuilder sb = new StringBuilder("Started task: ").append(taskName);
|
||||
if (options == null || options.isEmpty()) {
|
||||
return sb.toString();
|
||||
}
|
||||
Object bookIds = options.get("bookIds");
|
||||
Object libraryId = options.get("libraryId");
|
||||
if (bookIds instanceof Collection<?> ids && !ids.isEmpty()) {
|
||||
sb.append(" (Book IDs: ").append(ids.stream().map(Object::toString).collect(Collectors.joining(", "))).append(")");
|
||||
} else if (libraryId != null) {
|
||||
sb.append(" (Library ID: ").append(libraryId).append(")");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@@ -34,6 +34,8 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@@ -54,6 +56,7 @@ public class FileUploadService {
|
||||
private final AdditionalFileMapper additionalFileMapper;
|
||||
private final FileMovingHelper fileMovingHelper;
|
||||
private final MonitoringRegistrationService monitoringRegistrationService;
|
||||
private final AuditService auditService;
|
||||
|
||||
public void uploadFile(MultipartFile file, long libraryId, long pathId) {
|
||||
validateFile(file);
|
||||
@@ -78,6 +81,7 @@ public class FileUploadService {
|
||||
moveFileToFinalLocation(tempPath, finalPath);
|
||||
|
||||
log.info("File uploaded to final location: {}", finalPath);
|
||||
auditService.log(AuditAction.BOOK_UPLOADED, "Library", libraryId, "Uploaded file: " + originalFileName);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to upload file: {}", originalFileName, e);
|
||||
|
||||
@@ -19,6 +19,8 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -31,6 +33,7 @@ public class UserProvisioningService {
|
||||
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
||||
private final UserDefaultsService userDefaultsService;
|
||||
private final AppSettingService appSettingService;
|
||||
private final AuditService auditService;
|
||||
|
||||
public boolean isInitialUserAlreadyProvisioned() {
|
||||
return userRepository.count() > 0;
|
||||
@@ -264,6 +267,7 @@ public class UserProvisioningService {
|
||||
BookLoreUserEntity save = userRepository.save(user);
|
||||
userDefaultsService.addDefaultShelves(save);
|
||||
userDefaultsService.addDefaultSettings(save);
|
||||
auditService.log(AuditAction.USER_CREATED, "User", save.getId(), "Created user: " + save.getUsername());
|
||||
return save;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.booklore.model.enums.AuditAction;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -33,6 +35,7 @@ public class UserService {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final BookLoreUserTransformer bookLoreUserTransformer;
|
||||
private final AuditService auditService;
|
||||
|
||||
public List<BookLoreUser> getBookLoreUsers() {
|
||||
return userRepository.findAll()
|
||||
@@ -48,6 +51,7 @@ public class UserService {
|
||||
|
||||
if (updateRequest.getPermissions() != null && getMyself().getPermissions().isAdmin()) {
|
||||
UserPermission.copyFromRequestToEntity(updateRequest.getPermissions(), user.getPermissions());
|
||||
auditService.log(AuditAction.PERMISSIONS_CHANGED, "User", id, "Changed permissions for user: " + user.getUsername());
|
||||
}
|
||||
|
||||
if (updateRequest.getAssignedLibraries() != null && getMyself().getPermissions().isAdmin()) {
|
||||
@@ -57,6 +61,7 @@ public class UserService {
|
||||
}
|
||||
|
||||
userRepository.save(user);
|
||||
auditService.log(AuditAction.USER_UPDATED, "User", id, "Updated user: " + user.getUsername());
|
||||
return bookLoreUserTransformer.toDTO(user);
|
||||
}
|
||||
|
||||
@@ -71,6 +76,7 @@ public class UserService {
|
||||
throw ApiError.SELF_DELETION_NOT_ALLOWED.createException();
|
||||
}
|
||||
userRepository.delete(userToDelete);
|
||||
auditService.log(AuditAction.USER_DELETED, "User", id, "Deleted user: " + userToDelete.getUsername());
|
||||
}
|
||||
|
||||
public BookLoreUser getBookLoreUser(Long id) {
|
||||
@@ -107,6 +113,7 @@ public class UserService {
|
||||
bookLoreUserEntity.setDefaultPassword(false);
|
||||
bookLoreUserEntity.setPasswordHash(passwordEncoder.encode(changePasswordRequest.getNewPassword()));
|
||||
userRepository.save(bookLoreUserEntity);
|
||||
auditService.log(AuditAction.PASSWORD_CHANGED, "User", bookLoreUser.getId(), "Password changed by user: " + bookLoreUser.getUsername());
|
||||
}
|
||||
|
||||
public void changeUserPassword(ChangeUserPasswordRequest request) {
|
||||
@@ -116,6 +123,7 @@ public class UserService {
|
||||
}
|
||||
userEntity.setPasswordHash(passwordEncoder.encode(request.getNewPassword()));
|
||||
userRepository.save(userEntity);
|
||||
auditService.log(AuditAction.PASSWORD_CHANGED, "User", request.getUserId(), "Password changed for user: " + userEntity.getUsername());
|
||||
}
|
||||
|
||||
public void updateUserSetting(Long userId, UpdateUserSettingRequest request) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS audit_log
|
||||
(
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NULL,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
action VARCHAR(100) NOT NULL,
|
||||
entity_type VARCHAR(100) NULL,
|
||||
entity_id BIGINT NULL,
|
||||
description VARCHAR(1024) NOT NULL,
|
||||
ip_address VARCHAR(45) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log (created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_user_id ON audit_log (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log (action);
|
||||
@@ -11,6 +11,7 @@ import org.booklore.service.progress.ReadingProgressService;
|
||||
import org.booklore.service.metadata.sidecar.SidecarMetadataWriter;
|
||||
import org.booklore.service.monitoring.MonitoringRegistrationService;
|
||||
import org.booklore.service.FileStreamingService;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.util.FileService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -55,6 +56,7 @@ class BookServiceDeleteTests {
|
||||
BookUpdateService bookUpdateService = Mockito.mock(BookUpdateService.class);
|
||||
SidecarMetadataWriter sidecarMetadataWriter = Mockito.mock(SidecarMetadataWriter.class);
|
||||
FileStreamingService fileStreamingService = Mockito.mock(FileStreamingService.class);
|
||||
AuditService auditService = Mockito.mock(AuditService.class);
|
||||
|
||||
bookService = new BookService(
|
||||
bookRepository,
|
||||
@@ -73,7 +75,8 @@ class BookServiceDeleteTests {
|
||||
bookUpdateService,
|
||||
ebookViewerPreferenceRepository,
|
||||
sidecarMetadataWriter,
|
||||
fileStreamingService
|
||||
fileStreamingService,
|
||||
auditService
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.booklore.model.dto.BookMetadata;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookMetadataEntity;
|
||||
import org.booklore.repository.BookRepository;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.service.metadata.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -38,6 +39,8 @@ class MetadataControllerTest {
|
||||
private BookRepository bookRepository;
|
||||
@Mock
|
||||
private MetadataManagementService metadataManagementService;
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
private MetadataController metadataController;
|
||||
|
||||
@@ -2,10 +2,12 @@ package org.booklore.repository;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import org.booklore.BookloreApplication;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookMetadataEntity;
|
||||
import org.booklore.model.entity.LibraryEntity;
|
||||
import org.booklore.model.entity.LibraryPathEntity;
|
||||
import org.booklore.service.task.TaskCronService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@@ -19,11 +21,12 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
|
||||
|
||||
@SpringBootTest(classes = {
|
||||
org.booklore.BookloreApplication.class
|
||||
BookloreApplication.class
|
||||
})
|
||||
@Transactional
|
||||
@TestPropertySource(properties = {
|
||||
@@ -57,13 +60,13 @@ class BookOpdsRepositoryDataJpaTest {
|
||||
@Bean("flyway")
|
||||
@Primary
|
||||
public org.flywaydb.core.Flyway flyway() {
|
||||
return org.mockito.Mockito.mock(org.flywaydb.core.Flyway.class);
|
||||
return mock(org.flywaydb.core.Flyway.class);
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public org.booklore.service.task.TaskCronService taskCronService() {
|
||||
return org.mockito.Mockito.mock(org.booklore.service.task.TaskCronService.class);
|
||||
public TaskCronService taskCronService() {
|
||||
return mock(TaskCronService.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package org.booklore.service;
|
||||
|
||||
import org.booklore.config.BookmarkProperties;
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.exception.APIException;
|
||||
import org.booklore.mapper.BookMarkMapper;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.BookMark;
|
||||
import org.booklore.model.dto.CreateBookMarkRequest;
|
||||
import org.booklore.model.dto.UpdateBookMarkRequest;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.BookMarkEntity;
|
||||
@@ -112,7 +114,7 @@ class BookMarkServiceTest {
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(userDto);
|
||||
when(bookMarkRepository.existsByCfiAndBookIdAndUserId("new-cfi", bookId, userId)).thenReturn(true); // Duplicate exists
|
||||
|
||||
assertThrows(org.booklore.exception.APIException.class, () -> bookMarkService.createBookmark(request));
|
||||
assertThrows(APIException.class, () -> bookMarkService.createBookmark(request));
|
||||
verify(bookMarkRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@@ -153,7 +155,7 @@ class BookMarkServiceTest {
|
||||
|
||||
@Test
|
||||
void updateBookmark_Success() {
|
||||
var updateRequest = org.booklore.model.dto.UpdateBookMarkRequest.builder()
|
||||
var updateRequest = UpdateBookMarkRequest.builder()
|
||||
.title("Updated Title")
|
||||
.color("#FF0000")
|
||||
.notes("Updated notes")
|
||||
@@ -174,7 +176,7 @@ class BookMarkServiceTest {
|
||||
|
||||
@Test
|
||||
void updateBookmark_NotFound() {
|
||||
var updateRequest = org.booklore.model.dto.UpdateBookMarkRequest.builder()
|
||||
var updateRequest = UpdateBookMarkRequest.builder()
|
||||
.title("Updated Title")
|
||||
.build();
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ package org.booklore.service;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import org.booklore.BookloreApplication;
|
||||
import org.booklore.model.dto.*;
|
||||
import org.booklore.model.entity.*;
|
||||
import org.booklore.model.enums.BookFileType;
|
||||
import org.booklore.model.enums.ComicCreatorRole;
|
||||
import org.booklore.model.enums.ReadStatus;
|
||||
import org.booklore.repository.BookRepository;
|
||||
import org.booklore.service.task.TaskCronService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -30,8 +32,9 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@SpringBootTest(classes = {org.booklore.BookloreApplication.class})
|
||||
@SpringBootTest(classes = {BookloreApplication.class})
|
||||
@Transactional
|
||||
@TestPropertySource(properties = {
|
||||
"spring.flyway.enabled=false",
|
||||
@@ -58,13 +61,13 @@ class BookRuleEvaluatorServiceIntegrationTest {
|
||||
@Bean("flyway")
|
||||
@Primary
|
||||
public org.flywaydb.core.Flyway flyway() {
|
||||
return org.mockito.Mockito.mock(org.flywaydb.core.Flyway.class);
|
||||
return mock(org.flywaydb.core.Flyway.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public org.booklore.service.task.TaskCronService taskCronService() {
|
||||
return org.mockito.Mockito.mock(org.booklore.service.task.TaskCronService.class);
|
||||
public TaskCronService taskCronService() {
|
||||
return mock(TaskCronService.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.booklore.service;
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.model.dto.Book;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.settings.AppSettings;
|
||||
import org.booklore.model.dto.kobo.KoboBookMetadata;
|
||||
import org.booklore.model.dto.kobo.KoboTag;
|
||||
import org.booklore.model.dto.kobo.KoboTagWrapper;
|
||||
@@ -144,8 +145,8 @@ class KoboEntitlementServiceTest {
|
||||
return book;
|
||||
}
|
||||
|
||||
private org.booklore.model.dto.settings.AppSettings createAppSettingsWithKoboSettings() {
|
||||
var appSettings = new org.booklore.model.dto.settings.AppSettings();
|
||||
private AppSettings createAppSettingsWithKoboSettings() {
|
||||
var appSettings = new AppSettings();
|
||||
KoboSettings koboSettings = KoboSettings.builder()
|
||||
.convertCbxToEpub(true)
|
||||
.conversionLimitInMbForCbx(50)
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.booklore.exception.APIException;
|
||||
import org.booklore.model.dto.progress.KoreaderProgress;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.KoreaderUserEntity;
|
||||
import org.booklore.model.entity.UserBookProgressEntity;
|
||||
import org.booklore.model.enums.ReadStatus;
|
||||
import org.booklore.repository.BookRepository;
|
||||
@@ -73,7 +74,7 @@ class KoreaderServiceTest {
|
||||
|
||||
@Test
|
||||
void authorizeUser_success() {
|
||||
var userEntity = new org.booklore.model.entity.KoreaderUserEntity();
|
||||
var userEntity = new KoreaderUserEntity();
|
||||
userEntity.setPasswordMD5("MD5PWD");
|
||||
when(koreaderUserRepo.findByUsername("u"))
|
||||
.thenReturn(Optional.of(userEntity));
|
||||
@@ -93,7 +94,7 @@ class KoreaderServiceTest {
|
||||
|
||||
@Test
|
||||
void authorizeUser_badPassword() {
|
||||
var userEntity = new org.booklore.model.entity.KoreaderUserEntity();
|
||||
var userEntity = new KoreaderUserEntity();
|
||||
userEntity.setPasswordMD5("OTHER");
|
||||
when(koreaderUserRepo.findByUsername("u"))
|
||||
.thenReturn(Optional.of(userEntity));
|
||||
|
||||
@@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.exception.APIException;
|
||||
import org.booklore.mapper.KoreaderUserMapper;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.KoreaderUser;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
@@ -29,7 +31,7 @@ class KoreaderUserServiceTest {
|
||||
@Mock AuthenticationService authService;
|
||||
@Mock UserRepository userRepository;
|
||||
@Mock KoreaderUserRepository koreaderUserRepository;
|
||||
@Mock org.booklore.mapper.KoreaderUserMapper koreaderUserMapper;
|
||||
@Mock KoreaderUserMapper koreaderUserMapper;
|
||||
@InjectMocks
|
||||
KoreaderUserService service;
|
||||
|
||||
@@ -100,7 +102,7 @@ class KoreaderUserServiceTest {
|
||||
@Test
|
||||
void upsertUser_throws_whenOwnerMissing() {
|
||||
when(userRepository.findById(123L)).thenReturn(Optional.empty());
|
||||
assertThrows(org.booklore.exception.APIException.class,
|
||||
assertThrows(APIException.class,
|
||||
() -> service.upsertUser("x", "y"));
|
||||
}
|
||||
|
||||
@@ -114,7 +116,7 @@ class KoreaderUserServiceTest {
|
||||
@Test
|
||||
void getUser_throws_whenNotFound() {
|
||||
when(koreaderUserRepository.findByBookLoreUserId(123L)).thenReturn(Optional.empty());
|
||||
assertThrows(org.booklore.exception.APIException.class, () -> service.getUser());
|
||||
assertThrows(APIException.class, () -> service.getUser());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -128,6 +130,6 @@ class KoreaderUserServiceTest {
|
||||
@Test
|
||||
void toggleSync_throws_whenEntityMissing() {
|
||||
when(koreaderUserRepository.findByBookLoreUserId(123L)).thenReturn(Optional.empty());
|
||||
assertThrows(org.booklore.exception.APIException.class, () -> service.toggleSync(false));
|
||||
assertThrows(APIException.class, () -> service.toggleSync(false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.booklore.service;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.model.dto.MagicShelf;
|
||||
import org.booklore.model.entity.MagicShelfEntity;
|
||||
import org.booklore.model.enums.IconType;
|
||||
@@ -28,6 +29,8 @@ class MagicShelfServiceTest {
|
||||
private MagicShelfRepository magicShelfRepository;
|
||||
@Mock
|
||||
private AuthenticationService authenticationService;
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
private MagicShelfService magicShelfService;
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.booklore.model.dto.*;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.ShelfEntity;
|
||||
import org.booklore.model.entity.UserPermissionsEntity;
|
||||
import org.booklore.repository.BookOpdsRepository;
|
||||
import org.booklore.repository.ShelfRepository;
|
||||
import org.booklore.repository.UserRepository;
|
||||
@@ -123,7 +124,7 @@ class OpdsBookServiceTest {
|
||||
void getBooksPage_legacyUser_delegatesToLegacyMethod() {
|
||||
OpdsUserDetails details = legacyUserDetails();
|
||||
BookLoreUserEntity entity = mock(BookLoreUserEntity.class);
|
||||
var permissionsEntity = mock(org.booklore.model.entity.UserPermissionsEntity.class);
|
||||
var permissionsEntity = mock(UserPermissionsEntity.class);
|
||||
when(permissionsEntity.isPermissionAccessOpds()).thenReturn(true);
|
||||
when(permissionsEntity.isPermissionAdmin()).thenReturn(false);
|
||||
when(entity.getPermissions()).thenReturn(permissionsEntity);
|
||||
@@ -182,7 +183,7 @@ class OpdsBookServiceTest {
|
||||
when(bookOpdsRepository.findAllWithFullMetadataByIdsAndShelfIds(anyList(), anySet())).thenReturn(List.of());
|
||||
|
||||
BookLoreUserEntity entity = mock(BookLoreUserEntity.class);
|
||||
var permissionsEntity = mock(org.booklore.model.entity.UserPermissionsEntity.class);
|
||||
var permissionsEntity = mock(UserPermissionsEntity.class);
|
||||
when(permissionsEntity.isPermissionAccessOpds()).thenReturn(true);
|
||||
when(permissionsEntity.isPermissionAdmin()).thenReturn(true);
|
||||
when(entity.getPermissions()).thenReturn(permissionsEntity);
|
||||
@@ -329,7 +330,7 @@ class OpdsBookServiceTest {
|
||||
void getBooksPageForV2User_throwsForbidden_whenNoPermission() {
|
||||
OpdsUserV2 v2 = OpdsUserV2.builder().userId(1L).build();
|
||||
BookLoreUserEntity entity = mock(BookLoreUserEntity.class);
|
||||
var permissionsEntity = mock(org.booklore.model.entity.UserPermissionsEntity.class);
|
||||
var permissionsEntity = mock(UserPermissionsEntity.class);
|
||||
when(permissionsEntity.isPermissionAccessOpds()).thenReturn(false);
|
||||
when(permissionsEntity.isPermissionAdmin()).thenReturn(false);
|
||||
when(entity.getPermissions()).thenReturn(permissionsEntity);
|
||||
@@ -344,7 +345,7 @@ class OpdsBookServiceTest {
|
||||
void getBooksPage_withSingleShelfId_returnsShelfBooks() {
|
||||
OpdsUserDetails details = v2UserDetails(1L, false, Set.of(1L));
|
||||
BookLoreUserEntity entity = mock(BookLoreUserEntity.class);
|
||||
var permissionsEntity = mock(org.booklore.model.entity.UserPermissionsEntity.class);
|
||||
var permissionsEntity = mock(UserPermissionsEntity.class);
|
||||
when(permissionsEntity.isPermissionAccessOpds()).thenReturn(true);
|
||||
when(permissionsEntity.isPermissionAdmin()).thenReturn(false);
|
||||
when(entity.getPermissions()).thenReturn(permissionsEntity);
|
||||
@@ -383,7 +384,7 @@ class OpdsBookServiceTest {
|
||||
void getBooksPage_withMultipleShelfIds_returnsShelfBooks() {
|
||||
OpdsUserDetails details = v2UserDetails(1L, false, Set.of(1L));
|
||||
BookLoreUserEntity entity = mock(BookLoreUserEntity.class);
|
||||
var permissionsEntity = mock(org.booklore.model.entity.UserPermissionsEntity.class);
|
||||
var permissionsEntity = mock(UserPermissionsEntity.class);
|
||||
when(permissionsEntity.isPermissionAccessOpds()).thenReturn(true);
|
||||
when(permissionsEntity.isPermissionAdmin()).thenReturn(false);
|
||||
when(entity.getPermissions()).thenReturn(permissionsEntity);
|
||||
@@ -429,7 +430,7 @@ class OpdsBookServiceTest {
|
||||
void getBooksPage_withShelfIdAndQuery_searchesInShelf() {
|
||||
OpdsUserDetails details = v2UserDetails(1L, false, Set.of(1L));
|
||||
BookLoreUserEntity entity = mock(BookLoreUserEntity.class);
|
||||
var permissionsEntity = mock(org.booklore.model.entity.UserPermissionsEntity.class);
|
||||
var permissionsEntity = mock(UserPermissionsEntity.class);
|
||||
when(permissionsEntity.isPermissionAccessOpds()).thenReturn(true);
|
||||
when(permissionsEntity.isPermissionAdmin()).thenReturn(false);
|
||||
when(entity.getPermissions()).thenReturn(permissionsEntity);
|
||||
@@ -468,7 +469,7 @@ class OpdsBookServiceTest {
|
||||
void getBooksPage_withShelfId_throwsForbidden_whenNotOwner() {
|
||||
OpdsUserDetails details = v2UserDetails(1L, false, Set.of(1L));
|
||||
BookLoreUserEntity entity = mock(BookLoreUserEntity.class);
|
||||
var permissionsEntity = mock(org.booklore.model.entity.UserPermissionsEntity.class);
|
||||
var permissionsEntity = mock(UserPermissionsEntity.class);
|
||||
when(permissionsEntity.isPermissionAccessOpds()).thenReturn(true);
|
||||
when(permissionsEntity.isPermissionAdmin()).thenReturn(false);
|
||||
when(entity.getPermissions()).thenReturn(permissionsEntity);
|
||||
@@ -498,7 +499,7 @@ class OpdsBookServiceTest {
|
||||
void getBooksPage_withShelfId_allowsAdmin_evenIfNotOwner() {
|
||||
OpdsUserDetails details = v2UserDetails(1L, true, Set.of(1L));
|
||||
BookLoreUserEntity entity = mock(BookLoreUserEntity.class);
|
||||
var permissionsEntity = mock(org.booklore.model.entity.UserPermissionsEntity.class);
|
||||
var permissionsEntity = mock(UserPermissionsEntity.class);
|
||||
when(permissionsEntity.isPermissionAccessOpds()).thenReturn(true);
|
||||
when(permissionsEntity.isPermissionAdmin()).thenReturn(true);
|
||||
when(entity.getPermissions()).thenReturn(permissionsEntity);
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.booklore.service;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.mapper.OpdsUserV2Mapper;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.OpdsUserV2;
|
||||
import org.booklore.model.dto.request.OpdsUserV2CreateRequest;
|
||||
@@ -40,6 +41,8 @@ class OpdsUserV2ServiceTest {
|
||||
private OpdsUserV2Mapper mapper;
|
||||
@Mock
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
private OpdsUserV2Service service;
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.booklore.service;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.mapper.ShelfMapper;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.Shelf;
|
||||
import org.booklore.model.dto.request.ShelfCreateRequest;
|
||||
@@ -38,6 +39,8 @@ class ShelfServiceTest {
|
||||
private AuthenticationService authenticationService;
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
private ShelfService shelfService;
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.booklore.service.book;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.exception.APIException;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.mapper.BookMapper;
|
||||
import org.booklore.model.dto.*;
|
||||
import org.booklore.model.dto.request.ReadProgressRequest;
|
||||
@@ -69,6 +70,8 @@ class BookServiceTest {
|
||||
private MonitoringRegistrationService monitoringRegistrationService;
|
||||
@Mock
|
||||
private BookUpdateService bookUpdateService;
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
private BookService bookService;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.booklore.service.library;
|
||||
|
||||
import org.booklore.model.dto.settings.LibraryFile;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.LibraryEntity;
|
||||
import org.booklore.model.entity.LibraryPathEntity;
|
||||
@@ -89,7 +90,7 @@ class LibraryProcessingServiceRegressionTest {
|
||||
|
||||
when(libraryRepository.findById(libraryId)).thenReturn(Optional.of(libraryEntity));
|
||||
when(libraryFileHelper.getLibraryFiles(libraryEntity)).thenReturn(List.of(
|
||||
org.booklore.model.dto.settings.LibraryFile.builder()
|
||||
LibraryFile.builder()
|
||||
.libraryPathEntity(pathEntity)
|
||||
.fileName("other.epub")
|
||||
.fileSubPath("")
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.booklore.service.library;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.mapper.LibraryMapper;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.Library;
|
||||
import org.booklore.model.dto.LibraryPath;
|
||||
@@ -60,6 +61,8 @@ class LibraryServiceIconTest {
|
||||
private AuthenticationService authenticationService;
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
private LibraryService libraryService;
|
||||
|
||||
@@ -279,7 +279,7 @@ class BookCoverServiceTest {
|
||||
MetadataPersistenceSettings settings = new MetadataPersistenceSettings();
|
||||
settings.setSaveToOriginalFile(saveToOriginalFileObj);
|
||||
settings.setConvertCbrCb7ToCbz(convertCbrCb7ToCbz);
|
||||
return new org.booklore.model.dto.settings.AppSettings() {
|
||||
return new AppSettings() {
|
||||
@Override
|
||||
public MetadataPersistenceSettings getMetadataPersistenceSettings() {
|
||||
return settings;
|
||||
|
||||
@@ -388,8 +388,8 @@ class BookMetadataUpdaterTest {
|
||||
metadataEntity.setBook(bookEntity);
|
||||
bookEntity.setMetadata(metadataEntity);
|
||||
|
||||
Set<org.booklore.model.entity.AuthorEntity> existingAuthors = new HashSet<>();
|
||||
existingAuthors.add(org.booklore.model.entity.AuthorEntity.builder().name("Old Author").build());
|
||||
Set<AuthorEntity> existingAuthors = new HashSet<>();
|
||||
existingAuthors.add(AuthorEntity.builder().name("Old Author").build());
|
||||
metadataEntity.setAuthors(existingAuthors);
|
||||
|
||||
BookFileEntity primaryFile = new BookFileEntity();
|
||||
@@ -416,12 +416,12 @@ class BookMetadataUpdaterTest {
|
||||
.replaceMode(MetadataReplaceMode.REPLACE_ALL)
|
||||
.build();
|
||||
|
||||
when(authorRepository.findByName("New Author")).thenReturn(Optional.of(org.booklore.model.entity.AuthorEntity.builder().name("New Author").build()));
|
||||
when(authorRepository.findByName("New Author")).thenReturn(Optional.of(AuthorEntity.builder().name("New Author").build()));
|
||||
|
||||
bookMetadataUpdater.setBookMetadata(context);
|
||||
|
||||
// Verify authors are replaced
|
||||
Set<org.booklore.model.entity.AuthorEntity> authors = bookEntity.getMetadata().getAuthors();
|
||||
Set<AuthorEntity> authors = bookEntity.getMetadata().getAuthors();
|
||||
assertEquals(1, authors.size());
|
||||
assertTrue(authors.stream().anyMatch(a -> a.getName().equals("New Author")));
|
||||
assertFalse(authors.stream().anyMatch(a -> a.getName().equals("Old Author")));
|
||||
@@ -436,8 +436,8 @@ class BookMetadataUpdaterTest {
|
||||
metadataEntity.setBook(bookEntity);
|
||||
bookEntity.setMetadata(metadataEntity);
|
||||
|
||||
Set<org.booklore.model.entity.CategoryEntity> existingCategories = new HashSet<>();
|
||||
existingCategories.add(org.booklore.model.entity.CategoryEntity.builder().name("Old Category").build());
|
||||
Set<CategoryEntity> existingCategories = new HashSet<>();
|
||||
existingCategories.add(CategoryEntity.builder().name("Old Category").build());
|
||||
metadataEntity.setCategories(existingCategories);
|
||||
|
||||
BookFileEntity primaryFile = new BookFileEntity();
|
||||
@@ -464,12 +464,12 @@ class BookMetadataUpdaterTest {
|
||||
.replaceMode(MetadataReplaceMode.REPLACE_ALL)
|
||||
.build();
|
||||
|
||||
when(categoryRepository.findByName("New Category")).thenReturn(Optional.of(org.booklore.model.entity.CategoryEntity.builder().name("New Category").build()));
|
||||
when(categoryRepository.findByName("New Category")).thenReturn(Optional.of(CategoryEntity.builder().name("New Category").build()));
|
||||
|
||||
bookMetadataUpdater.setBookMetadata(context);
|
||||
|
||||
// Verify categories are replaced
|
||||
Set<org.booklore.model.entity.CategoryEntity> categories = bookEntity.getMetadata().getCategories();
|
||||
Set<CategoryEntity> categories = bookEntity.getMetadata().getCategories();
|
||||
assertEquals(1, categories.size());
|
||||
assertTrue(categories.stream().anyMatch(c -> c.getName().equals("New Category")));
|
||||
assertFalse(categories.stream().anyMatch(c -> c.getName().equals("Old Category")));
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.booklore.model.entity.ShelfEntity;
|
||||
import org.booklore.model.enums.BookFileType;
|
||||
import org.booklore.model.enums.OpdsSortOrder;
|
||||
import org.booklore.service.MagicShelfService;
|
||||
import org.booklore.util.ArchiveUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -480,7 +481,7 @@ class OpdsFeedServiceTest {
|
||||
BookFile bookFile = BookFile.builder()
|
||||
.bookType(BookFileType.CBX)
|
||||
.fileName("comic.cbz")
|
||||
.archiveType(org.booklore.util.ArchiveUtils.ArchiveType.UNKNOWN)
|
||||
.archiveType(ArchiveUtils.ArchiveType.UNKNOWN)
|
||||
.build();
|
||||
|
||||
String mimeType = (String) method.invoke(opdsFeedService, bookFile);
|
||||
@@ -495,7 +496,7 @@ class OpdsFeedServiceTest {
|
||||
BookFile bookFile = BookFile.builder()
|
||||
.bookType(BookFileType.CBX)
|
||||
.fileName("comic.cbr")
|
||||
.archiveType(org.booklore.util.ArchiveUtils.ArchiveType.UNKNOWN)
|
||||
.archiveType(ArchiveUtils.ArchiveType.UNKNOWN)
|
||||
.build();
|
||||
|
||||
String mimeType = (String) method.invoke(opdsFeedService, bookFile);
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.booklore.model.dto.response.AudiobookInfo;
|
||||
import org.booklore.model.dto.response.AudiobookTrack;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.model.entity.BookFileEntity;
|
||||
import org.booklore.model.entity.BookMetadataEntity;
|
||||
import org.jaudiotagger.audio.AudioFile;
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
import org.jaudiotagger.audio.AudioHeader;
|
||||
@@ -168,7 +169,7 @@ class AudioMetadataServiceTest {
|
||||
.build()
|
||||
));
|
||||
|
||||
var bookMetadata = new org.booklore.model.entity.BookMetadataEntity();
|
||||
var bookMetadata = new BookMetadataEntity();
|
||||
bookMetadata.setTitle("DB Book Title");
|
||||
bookMetadata.setNarrator("DB Narrator");
|
||||
bookEntity.setMetadata(bookMetadata);
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.booklore.exception.APIException;
|
||||
import org.booklore.exception.ApiError;
|
||||
import org.booklore.model.entity.BookEntity;
|
||||
import org.booklore.repository.BookRepository;
|
||||
@@ -296,7 +297,7 @@ class CbxReaderServiceTest {
|
||||
Files.createFile(unknownPath);
|
||||
Files.setLastModifiedTime(unknownPath, FileTime.fromMillis(System.currentTimeMillis()));
|
||||
|
||||
assertThrows(org.booklore.exception.APIException.class, () -> cbxReaderService.getAvailablePages(1L));
|
||||
assertThrows(APIException.class, () -> cbxReaderService.getAvailablePages(1L));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.booklore.service.task;
|
||||
|
||||
import org.booklore.repository.TaskHistoryRepository;
|
||||
import org.booklore.model.entity.TaskHistoryEntity;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.task.TaskStatus;
|
||||
import org.booklore.model.dto.response.TasksHistoryResponse;
|
||||
import org.booklore.model.enums.TaskType;
|
||||
@@ -22,6 +23,8 @@ class TaskHistoryServiceTest {
|
||||
|
||||
@Mock
|
||||
private TaskHistoryRepository taskHistoryRepository;
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
private TaskHistoryService taskHistoryService;
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.booklore.model.dto.response.CronConfig;
|
||||
import org.booklore.model.dto.response.TaskCreateResponse;
|
||||
import org.booklore.model.enums.TaskType;
|
||||
import org.booklore.task.TaskCancellationManager;
|
||||
import org.booklore.task.TaskStatus;
|
||||
import org.booklore.task.tasks.Task;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
@@ -197,7 +198,7 @@ class TaskServiceTest {
|
||||
TaskCreateResponse resp = taskService.runAsUser(req);
|
||||
|
||||
assertEquals(asyncType, resp.getTaskType());
|
||||
assertEquals(org.booklore.task.TaskStatus.ACCEPTED, resp.getStatus());
|
||||
assertEquals(TaskStatus.ACCEPTED, resp.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.booklore.service.file.FileMovingHelper;
|
||||
import org.booklore.model.dto.BookMetadata;
|
||||
import org.booklore.model.enums.BookFileExtension;
|
||||
import org.booklore.service.metadata.extractor.MetadataExtractorFactory;
|
||||
import org.booklore.service.audit.AuditService;
|
||||
import org.booklore.service.monitoring.MonitoringRegistrationService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -76,6 +77,8 @@ class FileUploadServiceTest {
|
||||
|
||||
@Mock
|
||||
MonitoringRegistrationService monitoringRegistrationService;
|
||||
@Mock
|
||||
AuditService auditService;
|
||||
|
||||
AppProperties appProperties;
|
||||
FileUploadService service;
|
||||
@@ -93,7 +96,7 @@ class FileUploadServiceTest {
|
||||
|
||||
service = new FileUploadService(
|
||||
libraryRepository, bookRepository, bookAdditionalFileRepository,
|
||||
appSettingService, appProperties, metadataExtractorFactory, additionalFileMapper, fileMovingHelper, monitoringRegistrationService
|
||||
appSettingService, appProperties, metadataExtractorFactory, additionalFileMapper, fileMovingHelper, monitoringRegistrationService, auditService
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user