mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 14:20:48 -05:00
Introduce more granular permission controls and update the user management UI (#1965)
Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
@@ -40,9 +40,14 @@ public class SecurityUtil {
|
||||
return user != null && user.getPermissions().isCanDownload();
|
||||
}
|
||||
|
||||
public boolean canManipulateLibrary() {
|
||||
public boolean canManageLibrary() {
|
||||
var user = getCurrentUser();
|
||||
return user != null && user.getPermissions().isCanManipulateLibrary();
|
||||
return user != null && user.getPermissions().isCanManageLibrary();
|
||||
}
|
||||
|
||||
public boolean canManageIcons() {
|
||||
var user = getCurrentUser();
|
||||
return user != null && user.getPermissions().isCanManageIcons();
|
||||
}
|
||||
|
||||
public boolean canSyncKoReader() {
|
||||
@@ -89,4 +94,19 @@ public class SecurityUtil {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canAccessBookdrop() {
|
||||
var user = getCurrentUser();
|
||||
return user != null && user.getPermissions().isCanAccessBookdrop();
|
||||
}
|
||||
|
||||
public boolean canAccessUserStats() {
|
||||
var user = getCurrentUser();
|
||||
return user != null && user.getPermissions().isCanAccessUserStats();
|
||||
}
|
||||
|
||||
public boolean canAccessTaskManager() {
|
||||
var user = getCurrentUser();
|
||||
return user != null && user.getPermissions().isCanAccessTaskManager();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
if (permissions.isPermissionEditMetadata()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_EDIT_METADATA"));
|
||||
}
|
||||
if (permissions.isPermissionManipulateLibrary()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_MANIPULATE_LIBRARY"));
|
||||
if (permissions.isPermissionManageLibrary()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_MANAGE_LIBRARY"));
|
||||
}
|
||||
if (permissions.isPermissionAdmin()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
|
||||
|
||||
@@ -89,7 +89,7 @@ public class KoboAuthFilter extends OncePerRequestFilter {
|
||||
addAuthorityIfPermissionGranted(authorities, "ROLE_UPLOAD", permissions.isPermissionUpload());
|
||||
addAuthorityIfPermissionGranted(authorities, "ROLE_DOWNLOAD", permissions.isPermissionDownload());
|
||||
addAuthorityIfPermissionGranted(authorities, "ROLE_EDIT_METADATA", permissions.isPermissionEditMetadata());
|
||||
addAuthorityIfPermissionGranted(authorities, "ROLE_MANIPULATE_LIBRARY", permissions.isPermissionManipulateLibrary());
|
||||
addAuthorityIfPermissionGranted(authorities, "ROLE_MANAGE_LIBRARY", permissions.isPermissionManageLibrary());
|
||||
addAuthorityIfPermissionGranted(authorities, "ROLE_ADMIN", permissions.isPermissionAdmin());
|
||||
addAuthorityIfPermissionGranted(authorities, "ROLE_SYNC_KOBO", permissions.isPermissionSyncKobo());
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public class AuthenticationService {
|
||||
permissions.setCanUpload(true);
|
||||
permissions.setCanDownload(true);
|
||||
permissions.setCanEditMetadata(true);
|
||||
permissions.setCanManipulateLibrary(true);
|
||||
permissions.setCanManageLibrary(true);
|
||||
permissions.setCanSyncKoReader(true);
|
||||
permissions.setCanSyncKobo(true);
|
||||
permissions.setCanEmailBook(true);
|
||||
|
||||
@@ -36,8 +36,7 @@ public class AppSettingController {
|
||||
@ApiResponse(responseCode = "400", description = "Invalid request")
|
||||
})
|
||||
@PutMapping
|
||||
public void updateSettings(
|
||||
@Parameter(description = "List of settings to update") @RequestBody List<SettingRequest> settingRequests) throws JsonProcessingException {
|
||||
public void updateSettings(@Parameter(description = "List of settings to update") @RequestBody List<SettingRequest> settingRequests) throws JsonProcessingException {
|
||||
for (SettingRequest settingRequest : settingRequests) {
|
||||
AppSettingKey key = AppSettingKey.valueOf(settingRequest.getName());
|
||||
appSettingService.updateSetting(key, settingRequest.getValue());
|
||||
|
||||
@@ -26,8 +26,7 @@ public class BackgroundUploadController {
|
||||
@Operation(summary = "Upload background image file", description = "Upload a new background image file for the authenticated user.")
|
||||
@ApiResponse(responseCode = "200", description = "Background image uploaded successfully")
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<UploadResponse> uploadFile(
|
||||
@Parameter(description = "Background image file") @RequestParam("file") MultipartFile file) {
|
||||
public ResponseEntity<UploadResponse> uploadFile(@Parameter(description = "Background image file") @RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
BookLoreUser authenticatedUser = authenticationService.getAuthenticatedUser();
|
||||
UploadResponse response = backgroundUploadService.uploadBackgroundFile(file, authenticatedUser.getId());
|
||||
|
||||
@@ -23,6 +23,7 @@ import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "Bookdrop", description = "Endpoints for managing bookdrop files and imports")
|
||||
@@ -39,6 +40,7 @@ public class BookdropFileController {
|
||||
@Operation(summary = "Get bookdrop notification summary", description = "Retrieve a summary of bookdrop file notifications.")
|
||||
@ApiResponse(responseCode = "200", description = "Notification summary returned successfully")
|
||||
@GetMapping("/notification")
|
||||
@PreAuthorize("@securityUtil.canAccessBookdrop() or @securityUtil.isAdmin()")
|
||||
public BookdropFileNotification getSummary() {
|
||||
return bookDropService.getFileNotificationSummary();
|
||||
}
|
||||
@@ -46,6 +48,7 @@ public class BookdropFileController {
|
||||
@Operation(summary = "Get bookdrop files by status", description = "Retrieve a paginated list of bookdrop files filtered by status.")
|
||||
@ApiResponse(responseCode = "200", description = "Bookdrop files returned successfully")
|
||||
@GetMapping("/files")
|
||||
@PreAuthorize("@securityUtil.canAccessBookdrop() or @securityUtil.isAdmin()")
|
||||
public Page<BookdropFile> getFilesByStatus(
|
||||
@Parameter(description = "Status to filter files by") @RequestParam(required = false) String status,
|
||||
Pageable pageable) {
|
||||
@@ -55,6 +58,7 @@ public class BookdropFileController {
|
||||
@Operation(summary = "Discard selected bookdrop files", description = "Discard selected bookdrop files based on selection criteria.")
|
||||
@ApiResponse(responseCode = "200", description = "Files discarded successfully")
|
||||
@PostMapping("/files/discard")
|
||||
@PreAuthorize("@securityUtil.canAccessBookdrop() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<Void> discardSelectedFiles(
|
||||
@Parameter(description = "Selection request for files to discard") @RequestBody BookdropSelectionRequest request) {
|
||||
bookDropService.discardSelectedFiles(request.isSelectAll(), request.getExcludedIds(), request.getSelectedIds());
|
||||
@@ -64,6 +68,7 @@ public class BookdropFileController {
|
||||
@Operation(summary = "Finalize bookdrop import", description = "Finalize the import of selected bookdrop files.")
|
||||
@ApiResponse(responseCode = "200", description = "Import finalized successfully")
|
||||
@PostMapping("/imports/finalize")
|
||||
@PreAuthorize("@securityUtil.canAccessBookdrop() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<BookdropFinalizeResult> finalizeImport(
|
||||
@Parameter(description = "Finalize import request") @RequestBody BookdropFinalizeRequest request) {
|
||||
BookdropFinalizeResult result = bookDropService.finalizeImport(request);
|
||||
@@ -73,6 +78,7 @@ public class BookdropFileController {
|
||||
@Operation(summary = "Rescan bookdrop folder", description = "Trigger a rescan of the bookdrop folder for new files.")
|
||||
@ApiResponse(responseCode = "200", description = "Bookdrop folder rescanned successfully")
|
||||
@PostMapping("/rescan")
|
||||
@PreAuthorize("@securityUtil.canAccessBookdrop() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<Void> rescanBookdrop() {
|
||||
monitoringService.rescanBookdropFolder();
|
||||
return ResponseEntity.ok().build();
|
||||
@@ -81,6 +87,7 @@ public class BookdropFileController {
|
||||
@Operation(summary = "Extract metadata from filenames using pattern", description = "Parse filenames of selected files using a pattern to extract metadata fields.")
|
||||
@ApiResponse(responseCode = "200", description = "Pattern extraction completed")
|
||||
@PostMapping("/files/extract-pattern")
|
||||
@PreAuthorize("@securityUtil.canAccessBookdrop() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<BookdropPatternExtractResult> extractFromPattern(
|
||||
@Parameter(description = "Pattern extraction request") @Valid @RequestBody BookdropPatternExtractRequest request) {
|
||||
BookdropPatternExtractResult result = filenamePatternExtractor.bulkExtract(request);
|
||||
@@ -90,6 +97,7 @@ public class BookdropFileController {
|
||||
@Operation(summary = "Bulk edit metadata for selected files", description = "Apply metadata changes to multiple selected files at once.")
|
||||
@ApiResponse(responseCode = "200", description = "Bulk edit completed")
|
||||
@PostMapping("/files/bulk-edit")
|
||||
@PreAuthorize("@securityUtil.canAccessBookdrop() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<BookdropBulkEditResult> bulkEditMetadata(
|
||||
@Parameter(description = "Bulk edit request") @Valid @RequestBody BookdropBulkEditRequest request) {
|
||||
BookdropBulkEditResult result = bookdropBulkEditService.bulkEdit(request);
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -24,8 +25,8 @@ public class FileMoveController {
|
||||
@Operation(summary = "Move files", description = "Bulk move files to a different location within the library.")
|
||||
@ApiResponse(responseCode = "200", description = "Files moved successfully")
|
||||
@PostMapping("/move")
|
||||
public ResponseEntity<?> moveFiles(
|
||||
@Parameter(description = "File move request") @RequestBody FileMoveRequest request) {
|
||||
@PreAuthorize("@securityUtil.canManageLibrary() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<?> moveFiles(@Parameter(description = "File move request") @RequestBody FileMoveRequest request) {
|
||||
fileMoveService.bulkMoveFiles(request);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "Icons", description = "Endpoints for managing SVG icons")
|
||||
@@ -25,6 +26,7 @@ public class IconController {
|
||||
@Operation(summary = "Save an SVG icon", description = "Saves an SVG icon to the system.")
|
||||
@ApiResponse(responseCode = "200", description = "SVG icon saved successfully")
|
||||
@PostMapping
|
||||
@PreAuthorize("@securityUtil.canManageIcons() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<?> saveSvgIcon(@Valid @RequestBody SvgIconCreateRequest svgIconCreateRequest) {
|
||||
iconService.saveSvgIcon(svgIconCreateRequest);
|
||||
return ResponseEntity.ok().build();
|
||||
@@ -33,6 +35,7 @@ public class IconController {
|
||||
@Operation(summary = "Save multiple SVG icons", description = "Saves multiple SVG icons to the system in batch.")
|
||||
@ApiResponse(responseCode = "200", description = "Batch save completed with detailed results")
|
||||
@PostMapping("/batch")
|
||||
@PreAuthorize("@securityUtil.canManageIcons() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<SvgIconBatchResponse> saveBatchSvgIcons(@Valid @RequestBody SvgIconBatchRequest request) {
|
||||
SvgIconBatchResponse response = iconService.saveBatchSvgIcons(request.getIcons());
|
||||
return ResponseEntity.ok(response);
|
||||
@@ -61,6 +64,7 @@ public class IconController {
|
||||
@Operation(summary = "Delete an SVG icon", description = "Deletes an SVG icon by its name.")
|
||||
@ApiResponse(responseCode = "200", description = "SVG icon deleted successfully")
|
||||
@DeleteMapping("/{svgName}")
|
||||
@PreAuthorize("@securityUtil.canManageIcons() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<?> deleteSvgIcon(@Parameter(description = "SVG icon name") @PathVariable String svgName) {
|
||||
iconService.deleteSvgIcon(svgName);
|
||||
return ResponseEntity.ok().build();
|
||||
|
||||
@@ -29,15 +29,13 @@ public class KoreaderController {
|
||||
@ApiResponse(responseCode = "200", description = "User authorized successfully")
|
||||
@GetMapping("/users/auth")
|
||||
public ResponseEntity<Map<String, String>> authorizeUser() {
|
||||
return koreaderService
|
||||
.authorizeUser();
|
||||
return koreaderService.authorizeUser();
|
||||
}
|
||||
|
||||
@Operation(summary = "Create KoReader user (disabled)", description = "Attempt to register a user via KoReader (always forbidden).")
|
||||
@ApiResponse(responseCode = "403", description = "User registration forbidden")
|
||||
@PostMapping("/users/create")
|
||||
public ResponseEntity<?> createUser(
|
||||
@Parameter(description = "User data") @RequestBody Map<String, Object> userData) {
|
||||
public ResponseEntity<?> createUser(@Parameter(description = "User data") @RequestBody Map<String, Object> userData) {
|
||||
log.warn("Attempt to register user via Koreader blocked: {}", userData);
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("error", "User registration via Koreader is disabled"));
|
||||
}
|
||||
@@ -45,8 +43,7 @@ public class KoreaderController {
|
||||
@Operation(summary = "Get KoReader progress", description = "Retrieve reading progress for a book by its hash.")
|
||||
@ApiResponse(responseCode = "200", description = "Progress returned successfully")
|
||||
@GetMapping("/syncs/progress/{bookHash}")
|
||||
public ResponseEntity<KoreaderProgress> getProgress(
|
||||
@Parameter(description = "Book hash") @PathVariable String bookHash) {
|
||||
public ResponseEntity<KoreaderProgress> getProgress(@Parameter(description = "Book hash") @PathVariable String bookHash) {
|
||||
KoreaderProgress progress = koreaderService.getProgress(bookHash);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -56,8 +53,7 @@ public class KoreaderController {
|
||||
@Operation(summary = "Update KoReader progress", description = "Update reading progress for a book.")
|
||||
@ApiResponse(responseCode = "200", description = "Progress updated successfully")
|
||||
@PutMapping("/syncs/progress")
|
||||
public ResponseEntity<?> updateProgress(
|
||||
@Parameter(description = "KoReader progress object") @Valid @RequestBody KoreaderProgress koreaderProgress) {
|
||||
public ResponseEntity<?> updateProgress(@Parameter(description = "KoReader progress object") @Valid @RequestBody KoreaderProgress koreaderProgress) {
|
||||
koreaderService.saveProgress(koreaderProgress.getDocument(), koreaderProgress);
|
||||
return ResponseEntity.ok(Map.of("status", "progress updated"));
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public class LibraryController {
|
||||
@Operation(summary = "Create a library", description = "Create a new library. Requires admin or manipulation permission.")
|
||||
@ApiResponse(responseCode = "200", description = "Library created successfully")
|
||||
@PostMapping
|
||||
@PreAuthorize("@securityUtil.canManipulateLibrary() or @securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canManageLibrary() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<Library> createLibrary(
|
||||
@Parameter(description = "Library creation request") @Validated @RequestBody CreateLibraryRequest request) {
|
||||
return ResponseEntity.ok(libraryService.createLibrary(request));
|
||||
@@ -59,7 +59,7 @@ public class LibraryController {
|
||||
@ApiResponse(responseCode = "200", description = "Library updated successfully")
|
||||
@PutMapping("/{libraryId}")
|
||||
@CheckLibraryAccess(libraryIdParam = "libraryId")
|
||||
@PreAuthorize("@securityUtil.canManipulateLibrary() or @securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canManageLibrary() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<Library> updateLibrary(
|
||||
@Parameter(description = "Library update request") @Validated @RequestBody CreateLibraryRequest request,
|
||||
@Parameter(description = "ID of the library") @PathVariable Long libraryId) {
|
||||
@@ -70,7 +70,7 @@ public class LibraryController {
|
||||
@ApiResponse(responseCode = "204", description = "Library deleted successfully")
|
||||
@DeleteMapping("/{libraryId}")
|
||||
@CheckLibraryAccess(libraryIdParam = "libraryId")
|
||||
@PreAuthorize("@securityUtil.canManipulateLibrary() or @securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canManageLibrary() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<?> deleteLibrary(
|
||||
@Parameter(description = "ID of the library") @PathVariable long libraryId) {
|
||||
libraryService.deleteLibrary(libraryId);
|
||||
@@ -101,9 +101,8 @@ public class LibraryController {
|
||||
@ApiResponse(responseCode = "204", description = "Library rescanned successfully")
|
||||
@PutMapping("/{libraryId}/refresh")
|
||||
@CheckLibraryAccess(libraryIdParam = "libraryId")
|
||||
@PreAuthorize("@securityUtil.canManipulateLibrary() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<?> rescanLibrary(
|
||||
@Parameter(description = "ID of the library") @PathVariable long libraryId) {
|
||||
@PreAuthorize("@securityUtil.canManageLibrary() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<?> rescanLibrary(@Parameter(description = "ID of the library") @PathVariable long libraryId) {
|
||||
libraryService.rescanLibrary(libraryId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
@@ -112,7 +111,7 @@ public class LibraryController {
|
||||
@ApiResponse(responseCode = "200", description = "File naming pattern updated successfully")
|
||||
@PatchMapping("/{libraryId}/file-naming-pattern")
|
||||
@CheckLibraryAccess(libraryIdParam = "libraryId")
|
||||
@PreAuthorize("@securityUtil.canManipulateLibrary() or @securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canManageLibrary() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<Library> setFileNamingPattern(
|
||||
@Parameter(description = "ID of the library") @PathVariable long libraryId,
|
||||
@Parameter(description = "File naming pattern body") @RequestBody Map<String, String> body) {
|
||||
|
||||
@@ -33,24 +33,21 @@ public class MagicShelfController {
|
||||
@Operation(summary = "Get a magic shelf by ID", description = "Retrieve a specific magic shelf by its ID.")
|
||||
@ApiResponse(responseCode = "200", description = "Magic shelf returned successfully")
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<MagicShelf> getShelf(
|
||||
@Parameter(description = "ID of the magic shelf") @PathVariable Long id) {
|
||||
public ResponseEntity<MagicShelf> getShelf(@Parameter(description = "ID of the magic shelf") @PathVariable Long id) {
|
||||
return ResponseEntity.ok(magicShelfService.getShelf(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "Create or update a magic shelf", description = "Create or update a magic shelf for the user.")
|
||||
@ApiResponse(responseCode = "200", description = "Magic shelf created/updated successfully")
|
||||
@PostMapping
|
||||
public ResponseEntity<MagicShelf> createUpdateShelf(
|
||||
@Parameter(description = "Magic shelf object") @Valid @RequestBody MagicShelf shelf) {
|
||||
public ResponseEntity<MagicShelf> createUpdateShelf(@Parameter(description = "Magic shelf object") @Valid @RequestBody MagicShelf shelf) {
|
||||
return ResponseEntity.ok(magicShelfService.createOrUpdateShelf(shelf));
|
||||
}
|
||||
|
||||
@Operation(summary = "Delete a magic shelf", description = "Delete a specific magic shelf by its ID.")
|
||||
@ApiResponse(responseCode = "204", description = "Magic shelf deleted successfully")
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteShelf(
|
||||
@Parameter(description = "ID of the magic shelf") @PathVariable Long id) {
|
||||
public ResponseEntity<Void> deleteShelf(@Parameter(description = "ID of the magic shelf") @PathVariable Long id) {
|
||||
magicShelfService.deleteShelf(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.adityachandel.booklore.controller;
|
||||
|
||||
import com.adityachandel.booklore.config.security.service.AuthenticationService;
|
||||
import com.adityachandel.booklore.config.security.annotation.CheckBookAccess;
|
||||
import com.adityachandel.booklore.exception.ApiError;
|
||||
import com.adityachandel.booklore.mapper.BookMetadataMapper;
|
||||
@@ -13,6 +12,10 @@ import com.adityachandel.booklore.model.entity.BookEntity;
|
||||
import com.adityachandel.booklore.model.enums.MetadataReplaceMode;
|
||||
import com.adityachandel.booklore.repository.BookRepository;
|
||||
import com.adityachandel.booklore.service.metadata.*;
|
||||
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.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -20,12 +23,6 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -37,7 +34,6 @@ public class MetadataController {
|
||||
|
||||
private final BookMetadataService bookMetadataService;
|
||||
private final BookMetadataUpdater bookMetadataUpdater;
|
||||
private final AuthenticationService authenticationService;
|
||||
private final BookMetadataMapper bookMetadataMapper;
|
||||
private final MetadataMatchService metadataMatchService;
|
||||
private final DuckDuckGoCoverService duckDuckGoCoverService;
|
||||
|
||||
@@ -59,7 +59,6 @@ public class MobileOidcController {
|
||||
private final UserRepository userRepository;
|
||||
private final UserProvisioningService userProvisioningService;
|
||||
private final AuthenticationService authenticationService;
|
||||
private final BookLoreUserTransformer bookLoreUserTransformer;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static final ConcurrentMap<String, Object> userLocks = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -26,9 +26,8 @@ public class PathController {
|
||||
@Operation(summary = "Get folders at a path", description = "Retrieve a list of folders at a given path. Requires admin or library manipulation permission.")
|
||||
@ApiResponse(responseCode = "200", description = "Folders returned successfully")
|
||||
@GetMapping
|
||||
@PreAuthorize("@securityUtil.canManipulateLibrary() or @securityUtil.isAdmin()")
|
||||
public List<String> getFolders(
|
||||
@Parameter(description = "Path to list folders at") @RequestParam String path) {
|
||||
@PreAuthorize("@securityUtil.canManageLibrary() or @securityUtil.isAdmin()")
|
||||
public List<String> getFolders(@Parameter(description = "Path to list folders at") @RequestParam String path) {
|
||||
return pathService.getFoldersAtPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
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;
|
||||
@@ -40,6 +41,7 @@ public class ReadingSessionController {
|
||||
@ApiResponse(responseCode = "401", description = "Unauthorized")
|
||||
})
|
||||
@GetMapping("/heatmap/year/{year}")
|
||||
@PreAuthorize("@securityUtil.canAccessUserStats() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<List<ReadingSessionHeatmapResponse>> getHeatmapForYear(@PathVariable int year) {
|
||||
List<ReadingSessionHeatmapResponse> heatmapData = readingSessionService.getSessionHeatmapForYear(year);
|
||||
return ResponseEntity.ok(heatmapData);
|
||||
@@ -52,6 +54,7 @@ public class ReadingSessionController {
|
||||
@ApiResponse(responseCode = "401", description = "Unauthorized")
|
||||
})
|
||||
@GetMapping("/timeline/week/{year}/{week}")
|
||||
@PreAuthorize("@securityUtil.canAccessUserStats() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<List<ReadingSessionTimelineResponse>> getTimelineForWeek(
|
||||
@PathVariable int year,
|
||||
@PathVariable int week) {
|
||||
|
||||
@@ -30,14 +30,14 @@ public class TaskController {
|
||||
private final TaskCronService taskCronService;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canAccessTaskManager() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<List<TaskInfo>> getAvailableTasks() {
|
||||
List<TaskInfo> taskInfos = service.getAvailableTasks();
|
||||
return ResponseEntity.ok(taskInfos);
|
||||
}
|
||||
|
||||
@PostMapping("/start")
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canAccessTaskManager() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<TaskCreateResponse> startTask(@RequestBody TaskCreateRequest request) {
|
||||
TaskCreateResponse response = service.runAsUser(request);
|
||||
if (response.getStatus() == TaskStatus.ACCEPTED) {
|
||||
@@ -47,21 +47,21 @@ public class TaskController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{taskId}/cancel")
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canAccessTaskManager() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<TaskCancelResponse> cancelTask(@PathVariable String taskId) {
|
||||
TaskCancelResponse response = service.cancelTask(taskId);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/last")
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canAccessTaskManager() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<TasksHistoryResponse> getLatestTasksForEachType() {
|
||||
TasksHistoryResponse response = taskHistoryService.getLatestTasksForEachType();
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PatchMapping("/{taskType}/cron")
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
@PreAuthorize("@securityUtil.canAccessTaskManager() or @securityUtil.isAdmin()")
|
||||
public ResponseEntity<CronConfig> patchCronConfig(@PathVariable TaskType taskType, @RequestBody TaskCronConfigRequest request) {
|
||||
CronConfig response = taskCronService.patchCronConfig(taskType, request);
|
||||
service.rescheduleTask(taskType);
|
||||
|
||||
@@ -41,8 +41,7 @@ public class UserController {
|
||||
})
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@securityUtil.canViewUserProfile(#id)")
|
||||
public ResponseEntity<BookLoreUser> getUser(
|
||||
@Parameter(description = "ID of the user") @PathVariable Long id) {
|
||||
public ResponseEntity<BookLoreUser> getUser(@Parameter(description = "ID of the user") @PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.getBookLoreUser(id));
|
||||
}
|
||||
|
||||
@@ -69,16 +68,14 @@ public class UserController {
|
||||
@ApiResponse(responseCode = "204", description = "User deleted successfully")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
public void deleteUser(
|
||||
@Parameter(description = "ID of the user") @PathVariable Long id) {
|
||||
public void deleteUser(@Parameter(description = "ID of the user") @PathVariable Long id) {
|
||||
userService.deleteUser(id);
|
||||
}
|
||||
|
||||
@Operation(summary = "Change password", description = "Change the password for the current user.")
|
||||
@ApiResponse(responseCode = "200", description = "Password changed successfully")
|
||||
@PutMapping("/change-password")
|
||||
public ResponseEntity<?> changePassword(
|
||||
@Parameter(description = "Change password request") @RequestBody ChangePasswordRequest request) {
|
||||
public ResponseEntity<?> changePassword(@Parameter(description = "Change password request") @RequestBody ChangePasswordRequest request) {
|
||||
userService.changePassword(request);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
@@ -87,8 +84,7 @@ public class UserController {
|
||||
@ApiResponse(responseCode = "200", description = "Password changed successfully")
|
||||
@PutMapping("/change-user-password")
|
||||
@PreAuthorize("@securityUtil.isAdmin()")
|
||||
public ResponseEntity<?> changeUserPassword(
|
||||
@Parameter(description = "Change user password request") @RequestBody ChangeUserPasswordRequest request) {
|
||||
public ResponseEntity<?> changeUserPassword(@Parameter(description = "Change user password request") @RequestBody ChangeUserPasswordRequest request) {
|
||||
userService.changeUserPassword(request);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@@ -52,10 +52,11 @@ public enum ApiError {
|
||||
UNSUPPORTED_FILE_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "%s"),
|
||||
CONFLICT(HttpStatus.CONFLICT, "%s"),
|
||||
FILE_NOT_FOUND(HttpStatus.NOT_FOUND, "File not found: %s"),
|
||||
SHELF_CANNOT_BE_DELETED(HttpStatus.FORBIDDEN, "'%s' shelf can't be deleted" ),
|
||||
SHELF_CANNOT_BE_DELETED(HttpStatus.FORBIDDEN, "'%s' shelf can't be deleted"),
|
||||
TASK_NOT_FOUND(HttpStatus.NOT_FOUND, "Scheduled task not found: %s"),
|
||||
TASK_ALREADY_RUNNING(HttpStatus.CONFLICT, "Task is already running: %s"),
|
||||
ICON_ALREADY_EXISTS(HttpStatus.CONFLICT, "SVG icon with name '%s' already exists");
|
||||
ICON_ALREADY_EXISTS(HttpStatus.CONFLICT, "SVG icon with name '%s' already exists"),
|
||||
DEMO_USER_PASSWORD_CHANGE_NOT_ALLOWED(HttpStatus.FORBIDDEN, "Demo user password change not allowed.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private final String message;
|
||||
|
||||
@@ -31,10 +31,18 @@ public class BookLoreUserTransformer {
|
||||
permissions.setCanEditMetadata(userEntity.getPermissions().isPermissionEditMetadata());
|
||||
permissions.setCanEmailBook(userEntity.getPermissions().isPermissionEmailBook());
|
||||
permissions.setCanDeleteBook(userEntity.getPermissions().isPermissionDeleteBook());
|
||||
permissions.setCanManipulateLibrary(userEntity.getPermissions().isPermissionManipulateLibrary());
|
||||
permissions.setCanManageLibrary(userEntity.getPermissions().isPermissionManageLibrary());
|
||||
permissions.setCanAccessOpds(userEntity.getPermissions().isPermissionAccessOpds());
|
||||
permissions.setCanSyncKoReader(userEntity.getPermissions().isPermissionSyncKoreader());
|
||||
permissions.setCanSyncKobo(userEntity.getPermissions().isPermissionSyncKobo());
|
||||
permissions.setCanManageMetadataConfig(userEntity.getPermissions().isPermissionManageMetadataConfig());
|
||||
permissions.setCanAccessBookdrop(userEntity.getPermissions().isPermissionAccessBookdrop());
|
||||
permissions.setCanAccessLibraryStats(userEntity.getPermissions().isPermissionAccessLibraryStats());
|
||||
permissions.setCanAccessUserStats(userEntity.getPermissions().isPermissionAccessUserStats());
|
||||
permissions.setCanAccessTaskManager(userEntity.getPermissions().isPermissionAccessTaskManager());
|
||||
permissions.setCanManageGlobalPreferences(userEntity.getPermissions().isPermissionManageGlobalPreferences());
|
||||
permissions.setCanManageIcons(userEntity.getPermissions().isPermissionManageIcons());
|
||||
permissions.setDemoUser(userEntity.getPermissions().isPermissionDemoUser());
|
||||
|
||||
BookLoreUser bookLoreUser = new BookLoreUser();
|
||||
bookLoreUser.setId(userEntity.getId());
|
||||
@@ -63,7 +71,8 @@ public class BookLoreUserTransformer {
|
||||
case SIDEBAR_SHELF_SORTING -> userSettings.setSidebarShelfSorting(objectMapper.readValue(value, SidebarSortOption.class));
|
||||
case SIDEBAR_MAGIC_SHELF_SORTING -> userSettings.setSidebarMagicShelfSorting(objectMapper.readValue(value, SidebarSortOption.class));
|
||||
case ENTITY_VIEW_PREFERENCES -> userSettings.setEntityViewPreferences(objectMapper.readValue(value, BookLoreUser.UserSettings.EntityViewPreferences.class));
|
||||
case TABLE_COLUMN_PREFERENCE -> userSettings.setTableColumnPreference(objectMapper.readValue(value, new TypeReference<>() {}));
|
||||
case TABLE_COLUMN_PREFERENCE -> userSettings.setTableColumnPreference(objectMapper.readValue(value, new TypeReference<>() {
|
||||
}));
|
||||
case DASHBOARD_CONFIG -> userSettings.setDashboardConfig(objectMapper.readValue(value, BookLoreUser.UserSettings.DashboardConfig.class));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -30,12 +30,20 @@ public class BookLoreUser {
|
||||
private boolean canUpload;
|
||||
private boolean canDownload;
|
||||
private boolean canEditMetadata;
|
||||
private boolean canManipulateLibrary;
|
||||
private boolean canManageLibrary;
|
||||
private boolean canSyncKoReader;
|
||||
private boolean canSyncKobo;
|
||||
private boolean canEmailBook;
|
||||
private boolean canDeleteBook;
|
||||
private boolean canAccessOpds;
|
||||
private boolean canManageMetadataConfig;
|
||||
private boolean canAccessBookdrop;
|
||||
private boolean canAccessLibraryStats;
|
||||
private boolean canAccessUserStats;
|
||||
private boolean canAccessTaskManager;
|
||||
private boolean canManageGlobalPreferences;
|
||||
private boolean canManageIcons;
|
||||
private boolean isDemoUser;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -14,13 +14,20 @@ public class UserCreateRequest {
|
||||
private boolean permissionUpload;
|
||||
private boolean permissionDownload;
|
||||
private boolean permissionEditMetadata;
|
||||
private boolean permissionManipulateLibrary;
|
||||
private boolean permissionManageLibrary;
|
||||
private boolean permissionEmailBook;
|
||||
private boolean permissionDeleteBook;
|
||||
private boolean permissionAccessOpds;
|
||||
private boolean permissionSyncKoreader;
|
||||
private boolean permissionSyncKobo;
|
||||
private boolean permissionAdmin;
|
||||
private boolean permissionManageMetadataConfig;
|
||||
private boolean permissionAccessBookdrop;
|
||||
private boolean permissionAccessLibraryStats;
|
||||
private boolean permissionAccessUserStats;
|
||||
private boolean permissionAccessTaskManager;
|
||||
private boolean permissionManageGlobalPreferences;
|
||||
private boolean permissionManageIcons;
|
||||
|
||||
private Set<Long> selectedLibraries;
|
||||
}
|
||||
@@ -17,11 +17,18 @@ public class UserUpdateRequest {
|
||||
private boolean canUpload;
|
||||
private boolean canDownload;
|
||||
private boolean canEditMetadata;
|
||||
private boolean canManipulateLibrary;
|
||||
private boolean canManageLibrary;
|
||||
private boolean canEmailBook;
|
||||
private boolean canDeleteBook;
|
||||
private boolean canAccessOpds;
|
||||
private boolean canSyncKoReader;
|
||||
private boolean canSyncKobo;
|
||||
private boolean canManageMetadataConfig;
|
||||
private boolean canAccessBookdrop;
|
||||
private boolean canAccessLibraryStats;
|
||||
private boolean canAccessUserStats;
|
||||
private boolean canAccessTaskManager;
|
||||
private boolean canManageGlobalPreferences;
|
||||
private boolean canManageIcons;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,54 @@
|
||||
package com.adityachandel.booklore.model.dto.settings;
|
||||
|
||||
import com.adityachandel.booklore.model.enums.PermissionType;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
public enum AppSettingKey {
|
||||
OIDC_PROVIDER_DETAILS("oidc_provider_details", true, true),
|
||||
// @formatter:off
|
||||
// ADMIN only (public settings)
|
||||
OIDC_PROVIDER_DETAILS ("oidc_provider_details", true, true, List.of(PermissionType.ADMIN)),
|
||||
OIDC_ENABLED ("oidc_enabled", false, true, List.of(PermissionType.ADMIN)),
|
||||
OIDC_AUTO_PROVISION_DETAILS ("oidc_auto_provision_details", true, false, List.of(PermissionType.ADMIN)),
|
||||
KOBO_SETTINGS ("kobo_settings", true, false, List.of(PermissionType.ADMIN)),
|
||||
OPDS_SERVER_ENABLED ("opds_server_enabled", false, false, List.of(PermissionType.ADMIN)),
|
||||
|
||||
QUICK_BOOK_MATCH("quick_book_match", true, false),
|
||||
LIBRARY_METADATA_REFRESH_OPTIONS("library_metadata_refresh_options", true, false),
|
||||
OIDC_AUTO_PROVISION_DETAILS("oidc_auto_provision_details", true, false),
|
||||
SIDEBAR_LIBRARY_SORTING("sidebar_library_sorting", true, false),
|
||||
SIDEBAR_SHELF_SORTING("sidebar_shelf_sorting", true, false),
|
||||
METADATA_PROVIDER_SETTINGS("metadata_provider_settings", true, false),
|
||||
METADATA_MATCH_WEIGHTS("metadata_match_weights", true, false),
|
||||
METADATA_PERSISTENCE_SETTINGS("metadata_persistence_settings", true, false),
|
||||
METADATA_PUBLIC_REVIEWS_SETTINGS("metadata_public_reviews_settings", true, false),
|
||||
KOBO_SETTINGS("kobo_settings", true, false),
|
||||
COVER_CROPPING_SETTINGS("cover_cropping_settings", true, false),
|
||||
// ADMIN + MANAGE_METADATA_CONFIG
|
||||
QUICK_BOOK_MATCH ("quick_book_match", true, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
LIBRARY_METADATA_REFRESH_OPTIONS ("library_metadata_refresh_options", true, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
METADATA_PROVIDER_SETTINGS ("metadata_provider_settings", true, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
METADATA_MATCH_WEIGHTS ("metadata_match_weights", true, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
METADATA_PERSISTENCE_SETTINGS ("metadata_persistence_settings", true, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
METADATA_PUBLIC_REVIEWS_SETTINGS ("metadata_public_reviews_settings", true, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
UPLOAD_FILE_PATTERN ("upload_file_pattern", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
MOVE_FILE_PATTERN ("move_file_pattern", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
METADATA_DOWNLOAD_ON_BOOKDROP ("metadata_download_on_bookdrop", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_METADATA_CONFIG)),
|
||||
|
||||
AUTO_BOOK_SEARCH("auto_book_search", false, false),
|
||||
COVER_IMAGE_RESOLUTION("cover_image_resolution", false, false),
|
||||
SIMILAR_BOOK_RECOMMENDATION("similar_book_recommendation", false, false),
|
||||
UPLOAD_FILE_PATTERN("upload_file_pattern", false, false),
|
||||
MOVE_FILE_PATTERN("move_file_pattern", false, false),
|
||||
OPDS_SERVER_ENABLED("opds_server_enabled", false, false),
|
||||
OIDC_ENABLED("oidc_enabled", false, true),
|
||||
CBX_CACHE_SIZE_IN_MB("cbx_cache_size_in_mb", false, false),
|
||||
PDF_CACHE_SIZE_IN_MB("pdf_cache_size_in_mb", false, false),
|
||||
BOOK_DELETION_ENABLED("book_deletion_enabled", false, false),
|
||||
METADATA_DOWNLOAD_ON_BOOKDROP("metadata_download_on_bookdrop", false, false),
|
||||
MAX_FILE_UPLOAD_SIZE_IN_MB("max_file_upload_size_in_mb", false, false);
|
||||
// ADMIN + MANAGE_GLOBAL_PREFERENCES
|
||||
COVER_CROPPING_SETTINGS ("cover_cropping_settings", true, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)),
|
||||
AUTO_BOOK_SEARCH ("auto_book_search", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)),
|
||||
SIMILAR_BOOK_RECOMMENDATION ("similar_book_recommendation", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)),
|
||||
CBX_CACHE_SIZE_IN_MB ("cbx_cache_size_in_mb", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)),
|
||||
PDF_CACHE_SIZE_IN_MB ("pdf_cache_size_in_mb", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)),
|
||||
MAX_FILE_UPLOAD_SIZE_IN_MB ("max_file_upload_size_in_mb", false, false, List.of(PermissionType.ADMIN, PermissionType.MANAGE_GLOBAL_PREFERENCES)),
|
||||
|
||||
// No specific permissions required
|
||||
SIDEBAR_LIBRARY_SORTING ("sidebar_library_sorting", true, false, List.of()),
|
||||
SIDEBAR_SHELF_SORTING ("sidebar_shelf_sorting", true, false, List.of());
|
||||
// @formatter:on
|
||||
|
||||
private final String dbKey;
|
||||
private final boolean isJson;
|
||||
private final boolean isPublic;
|
||||
private final List<PermissionType> requiredPermissions;
|
||||
|
||||
AppSettingKey(String dbKey, boolean isJson, boolean isPublic) {
|
||||
AppSettingKey(String dbKey, boolean isJson, boolean isPublic, List<PermissionType> requiredPermissions) {
|
||||
this.dbKey = dbKey;
|
||||
this.isJson = isJson;
|
||||
this.isPublic = isPublic;
|
||||
this.requiredPermissions = requiredPermissions;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,7 +23,6 @@ public class AppSettings {
|
||||
private Integer pdfCacheSizeInMb;
|
||||
private Integer maxFileUploadSizeInMb;
|
||||
private boolean remoteAuthEnabled;
|
||||
private boolean bookDeletionEnabled;
|
||||
private boolean metadataDownloadOnBookdrop;
|
||||
private boolean oidcEnabled;
|
||||
private OidcProviderDetails oidcProviderDetails;
|
||||
|
||||
@@ -20,6 +20,9 @@ public class UserPermissionsEntity {
|
||||
@JoinColumn(name = "user_id", nullable = false, unique = true)
|
||||
private BookLoreUserEntity user;
|
||||
|
||||
@Column(name = "permission_admin", nullable = false)
|
||||
private boolean permissionAdmin;
|
||||
|
||||
@Column(name = "permission_upload", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionUpload = false;
|
||||
@@ -34,7 +37,7 @@ public class UserPermissionsEntity {
|
||||
|
||||
@Column(name = "permission_manipulate_library", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionManipulateLibrary = false;
|
||||
private boolean permissionManageLibrary = false;
|
||||
|
||||
@Column(name = "permission_email_book", nullable = false)
|
||||
@Builder.Default
|
||||
@@ -56,6 +59,35 @@ public class UserPermissionsEntity {
|
||||
@Builder.Default
|
||||
private boolean permissionSyncKobo = false;
|
||||
|
||||
@Column(name = "permission_admin", nullable = false)
|
||||
private boolean permissionAdmin;
|
||||
@Column(name = "permission_manage_metadata_config", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionManageMetadataConfig = false;
|
||||
|
||||
@Column(name = "permission_access_bookdrop", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionAccessBookdrop = false;
|
||||
|
||||
@Column(name = "permission_access_library_stats", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionAccessLibraryStats = false;
|
||||
|
||||
@Column(name = "permission_access_user_stats", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionAccessUserStats = false;
|
||||
|
||||
@Column(name = "permission_access_task_manager", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionAccessTaskManager = false;
|
||||
|
||||
@Column(name = "permission_manage_global_preferences", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionManageGlobalPreferences = false;
|
||||
|
||||
@Column(name = "permission_manage_icons", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionManageIcons = false;
|
||||
|
||||
@Column(name = "permission_demo_user", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean permissionDemoUser = false;
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
package com.adityachandel.booklore.model.enums;
|
||||
|
||||
public enum PermissionType {
|
||||
ADMIN,
|
||||
UPLOAD,
|
||||
DOWNLOAD,
|
||||
EDIT_METADATA,
|
||||
MANIPULATE_LIBRARY,
|
||||
MANAGE_LIBRARY,
|
||||
EMAIL_BOOK,
|
||||
DELETE_BOOK,
|
||||
SYNC_KOREADER,
|
||||
SYNC_KOBO,
|
||||
ACCESS_OPDS,
|
||||
ADMIN
|
||||
MANAGE_METADATA_CONFIG,
|
||||
ACCESS_BOOKDROP,
|
||||
ACCESS_LIBRARY_STATS,
|
||||
ACCESS_USER_STATS,
|
||||
ACCESS_TASK_MANAGER,
|
||||
MANAGE_GLOBAL_PREFERENCES,
|
||||
MANAGE_ICONS,
|
||||
DEMO_USER
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package com.adityachandel.booklore.service.appsettings;
|
||||
|
||||
import com.adityachandel.booklore.config.AppProperties;
|
||||
import com.adityachandel.booklore.config.security.service.AuthenticationService;
|
||||
import com.adityachandel.booklore.model.dto.BookLoreUser;
|
||||
import com.adityachandel.booklore.model.dto.request.MetadataRefreshOptions;
|
||||
import com.adityachandel.booklore.model.dto.settings.*;
|
||||
import com.adityachandel.booklore.model.entity.AppSettingEntity;
|
||||
import com.adityachandel.booklore.model.enums.PermissionType;
|
||||
import com.adityachandel.booklore.util.UserPermissionUtils;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
@@ -16,15 +21,21 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppSettingService {
|
||||
|
||||
private final AppProperties appProperties;
|
||||
private final SettingPersistenceHelper settingPersistenceHelper;
|
||||
private final AuthenticationService authenticationService;
|
||||
|
||||
private volatile AppSettings appSettings;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public AppSettingService(AppProperties appProperties, SettingPersistenceHelper settingPersistenceHelper, @Lazy AuthenticationService authenticationService) {
|
||||
this.appProperties = appProperties;
|
||||
this.settingPersistenceHelper = settingPersistenceHelper;
|
||||
this.authenticationService = authenticationService;
|
||||
}
|
||||
|
||||
public AppSettings getAppSettings() {
|
||||
if (appSettings == null) {
|
||||
lock.lock();
|
||||
@@ -41,6 +52,10 @@ public class AppSettingService {
|
||||
|
||||
@Transactional
|
||||
public void updateSetting(AppSettingKey key, Object val) throws JsonProcessingException {
|
||||
BookLoreUser user = authenticationService.getAuthenticatedUser();
|
||||
|
||||
validatePermission(key, user);
|
||||
|
||||
var setting = settingPersistenceHelper.appSettingsRepository.findByName(key.toString());
|
||||
if (setting == null) {
|
||||
setting = new AppSettingEntity();
|
||||
@@ -51,6 +66,21 @@ public class AppSettingService {
|
||||
refreshCache();
|
||||
}
|
||||
|
||||
private void validatePermission(AppSettingKey key, BookLoreUser user) {
|
||||
List<PermissionType> requiredPermissions = key.getRequiredPermissions();
|
||||
if (requiredPermissions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean hasPermission = requiredPermissions.stream().anyMatch(permission ->
|
||||
UserPermissionUtils.hasPermission(user.getPermissions(), permission)
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
throw new AccessDeniedException("User does not have permission to update " + key.getDbKey());
|
||||
}
|
||||
}
|
||||
|
||||
public PublicAppSetting getPublicSettings() {
|
||||
return buildPublicSetting();
|
||||
}
|
||||
@@ -104,7 +134,6 @@ public class AppSettingService {
|
||||
builder.cbxCacheSizeInMb(Integer.parseInt(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.CBX_CACHE_SIZE_IN_MB, "5120")));
|
||||
builder.pdfCacheSizeInMb(Integer.parseInt(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.PDF_CACHE_SIZE_IN_MB, "5120")));
|
||||
builder.maxFileUploadSizeInMb(Integer.parseInt(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.MAX_FILE_UPLOAD_SIZE_IN_MB, "100")));
|
||||
builder.bookDeletionEnabled(Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.BOOK_DELETION_ENABLED, "false")));
|
||||
builder.metadataDownloadOnBookdrop(Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.METADATA_DOWNLOAD_ON_BOOKDROP, "true")));
|
||||
|
||||
boolean settingEnabled = Boolean.parseBoolean(settingPersistenceHelper.getOrCreateSetting(AppSettingKey.OIDC_ENABLED, "false"));
|
||||
|
||||
@@ -52,7 +52,7 @@ public class BookReviewService {
|
||||
|
||||
// Check user permissions for auto-download
|
||||
BookLoreUser currentUser = authenticationService.getAuthenticatedUser();
|
||||
boolean hasPermission = currentUser.getPermissions().isAdmin() || currentUser.getPermissions().isCanManipulateLibrary();
|
||||
boolean hasPermission = currentUser.getPermissions().isAdmin() || currentUser.getPermissions().isCanManageLibrary();
|
||||
|
||||
if (!hasPermission || !reviewSettings.isAutoDownloadEnabled()) {
|
||||
return existingReviews;
|
||||
|
||||
@@ -106,7 +106,7 @@ public class BookdropEventHandlerService {
|
||||
notificationService.sendMessageToPermissions(
|
||||
Topic.LOG,
|
||||
LogNotification.info("Processing bookdrop file: " + fileName + " (" + queueSize + " files remaining)"),
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY)
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANAGE_LIBRARY)
|
||||
);
|
||||
|
||||
BookdropFileEntity bookdropFileEntity = BookdropFileEntity.builder()
|
||||
@@ -134,13 +134,13 @@ public class BookdropEventHandlerService {
|
||||
notificationService.sendMessageToPermissions(
|
||||
Topic.LOG,
|
||||
LogNotification.info("All bookdrop files have finished processing"),
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY)
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANAGE_LIBRARY)
|
||||
);
|
||||
} else {
|
||||
notificationService.sendMessageToPermissions(
|
||||
Topic.LOG,
|
||||
LogNotification.info("Finished processing bookdrop file: " + fileName + " (" + fileQueue.size() + " files remaining)"),
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY)
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANAGE_LIBRARY)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,6 @@ public class BookdropNotificationService {
|
||||
Instant.now().toString()
|
||||
);
|
||||
|
||||
notificationService.sendMessageToPermissions(Topic.BOOKDROP_FILE, summaryNotification, Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY));
|
||||
notificationService.sendMessageToPermissions(Topic.BOOKDROP_FILE, summaryNotification, Set.of(PermissionType.ADMIN, PermissionType.MANAGE_LIBRARY));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,12 +51,19 @@ public class UserProvisioningService {
|
||||
perms.setPermissionUpload(true);
|
||||
perms.setPermissionDownload(true);
|
||||
perms.setPermissionEditMetadata(true);
|
||||
perms.setPermissionManipulateLibrary(true);
|
||||
perms.setPermissionManageLibrary(true);
|
||||
perms.setPermissionEmailBook(true);
|
||||
perms.setPermissionDeleteBook(true);
|
||||
perms.setPermissionAccessOpds(true);
|
||||
perms.setPermissionSyncKoreader(true);
|
||||
perms.setPermissionSyncKobo(true);
|
||||
perms.setPermissionManageMetadataConfig(true);
|
||||
perms.setPermissionAccessBookdrop(true);
|
||||
perms.setPermissionAccessLibraryStats(true);
|
||||
perms.setPermissionAccessUserStats(true);
|
||||
perms.setPermissionAccessTaskManager(true);
|
||||
perms.setPermissionManageGlobalPreferences(true);
|
||||
perms.setPermissionManageIcons(true);
|
||||
|
||||
user.setPermissions(perms);
|
||||
createUser(user);
|
||||
@@ -82,13 +89,20 @@ public class UserProvisioningService {
|
||||
permissions.setPermissionUpload(request.isPermissionUpload());
|
||||
permissions.setPermissionDownload(request.isPermissionDownload());
|
||||
permissions.setPermissionEditMetadata(request.isPermissionEditMetadata());
|
||||
permissions.setPermissionManipulateLibrary(request.isPermissionManipulateLibrary());
|
||||
permissions.setPermissionManageLibrary(request.isPermissionManageLibrary());
|
||||
permissions.setPermissionEmailBook(request.isPermissionEmailBook());
|
||||
permissions.setPermissionDeleteBook(request.isPermissionDeleteBook());
|
||||
permissions.setPermissionAccessOpds(request.isPermissionAccessOpds());
|
||||
permissions.setPermissionSyncKoreader(request.isPermissionSyncKoreader());
|
||||
permissions.setPermissionSyncKobo(request.isPermissionSyncKobo());
|
||||
permissions.setPermissionAdmin(request.isPermissionAdmin());
|
||||
permissions.setPermissionManageMetadataConfig(request.isPermissionManageMetadataConfig());
|
||||
permissions.setPermissionAccessBookdrop(request.isPermissionAccessBookdrop());
|
||||
permissions.setPermissionAccessLibraryStats(request.isPermissionAccessLibraryStats());
|
||||
permissions.setPermissionAccessUserStats(request.isPermissionAccessUserStats());
|
||||
permissions.setPermissionAccessTaskManager(request.isPermissionAccessTaskManager());
|
||||
permissions.setPermissionManageGlobalPreferences(request.isPermissionManageGlobalPreferences());
|
||||
permissions.setPermissionManageIcons(request.isPermissionManageIcons());
|
||||
user.setPermissions(permissions);
|
||||
|
||||
if (request.getSelectedLibraries() != null && !request.getSelectedLibraries().isEmpty()) {
|
||||
@@ -115,12 +129,19 @@ public class UserProvisioningService {
|
||||
perms.setPermissionUpload(defaultPermissions.contains("permissionUpload"));
|
||||
perms.setPermissionDownload(defaultPermissions.contains("permissionDownload"));
|
||||
perms.setPermissionEditMetadata(defaultPermissions.contains("permissionEditMetadata"));
|
||||
perms.setPermissionManipulateLibrary(defaultPermissions.contains("permissionManipulateLibrary"));
|
||||
perms.setPermissionManageLibrary(defaultPermissions.contains("permissionManageLibrary"));
|
||||
perms.setPermissionEmailBook(defaultPermissions.contains("permissionEmailBook"));
|
||||
perms.setPermissionDeleteBook(defaultPermissions.contains("permissionDeleteBook"));
|
||||
perms.setPermissionAccessOpds(defaultPermissions.contains("permissionAccessOpds"));
|
||||
perms.setPermissionSyncKoreader(defaultPermissions.contains("permissionSyncKoreader"));
|
||||
perms.setPermissionSyncKobo(defaultPermissions.contains("permissionSyncKobo"));
|
||||
perms.setPermissionManageMetadataConfig(defaultPermissions.contains("permissionManageMetadataConfig"));
|
||||
perms.setPermissionAccessBookdrop(defaultPermissions.contains("permissionAccessBookdrop"));
|
||||
perms.setPermissionAccessLibraryStats(defaultPermissions.contains("permissionAccessLibraryStats"));
|
||||
perms.setPermissionAccessUserStats(defaultPermissions.contains("permissionAccessUserStats"));
|
||||
perms.setPermissionAccessTaskManager(defaultPermissions.contains("permissionAccessTaskManager"));
|
||||
perms.setPermissionManageGlobalPreferences(defaultPermissions.contains("permissionManageGlobalPreferences"));
|
||||
perms.setPermissionManageIcons(defaultPermissions.contains("permissionManageIcons"));
|
||||
}
|
||||
user.setPermissions(perms);
|
||||
|
||||
@@ -170,22 +191,36 @@ public class UserProvisioningService {
|
||||
permissions.setPermissionUpload(defaultPermissions.contains("permissionUpload"));
|
||||
permissions.setPermissionDownload(defaultPermissions.contains("permissionDownload"));
|
||||
permissions.setPermissionEditMetadata(defaultPermissions.contains("permissionEditMetadata"));
|
||||
permissions.setPermissionManipulateLibrary(defaultPermissions.contains("permissionManipulateLibrary"));
|
||||
permissions.setPermissionManageLibrary(defaultPermissions.contains("permissionManageLibrary"));
|
||||
permissions.setPermissionEmailBook(defaultPermissions.contains("permissionEmailBook"));
|
||||
permissions.setPermissionDeleteBook(defaultPermissions.contains("permissionDeleteBook"));
|
||||
permissions.setPermissionAccessOpds(defaultPermissions.contains("permissionAccessOpds"));
|
||||
permissions.setPermissionSyncKoreader(defaultPermissions.contains("permissionSyncKoreader"));
|
||||
permissions.setPermissionSyncKobo(defaultPermissions.contains("permissionSyncKobo"));
|
||||
permissions.setPermissionManageMetadataConfig(defaultPermissions.contains("permissionManageMetadataConfig"));
|
||||
permissions.setPermissionAccessBookdrop(defaultPermissions.contains("permissionAccessBookdrop"));
|
||||
permissions.setPermissionAccessLibraryStats(defaultPermissions.contains("permissionAccessLibraryStats"));
|
||||
permissions.setPermissionAccessUserStats(defaultPermissions.contains("permissionAccessUserStats"));
|
||||
permissions.setPermissionAccessTaskManager(defaultPermissions.contains("permissionAccessTaskManager"));
|
||||
permissions.setPermissionManageGlobalPreferences(defaultPermissions.contains("permissionManageGlobalPreferences"));
|
||||
permissions.setPermissionManageIcons(defaultPermissions.contains("permissionManageIcons"));
|
||||
} else {
|
||||
permissions.setPermissionUpload(false);
|
||||
permissions.setPermissionDownload(false);
|
||||
permissions.setPermissionEditMetadata(false);
|
||||
permissions.setPermissionManipulateLibrary(false);
|
||||
permissions.setPermissionManageLibrary(false);
|
||||
permissions.setPermissionEmailBook(false);
|
||||
permissions.setPermissionAccessOpds(false);
|
||||
permissions.setPermissionDeleteBook(false);
|
||||
permissions.setPermissionSyncKoreader(false);
|
||||
permissions.setPermissionSyncKobo(false);
|
||||
permissions.setPermissionManageMetadataConfig(false);
|
||||
permissions.setPermissionAccessBookdrop(false);
|
||||
permissions.setPermissionAccessLibraryStats(false);
|
||||
permissions.setPermissionAccessUserStats(false);
|
||||
permissions.setPermissionAccessTaskManager(false);
|
||||
permissions.setPermissionManageGlobalPreferences(false);
|
||||
permissions.setPermissionManageIcons(false);
|
||||
}
|
||||
|
||||
permissions.setPermissionAdmin(isAdmin);
|
||||
|
||||
@@ -50,12 +50,19 @@ public class UserService {
|
||||
user.getPermissions().setPermissionUpload(updateRequest.getPermissions().isCanUpload());
|
||||
user.getPermissions().setPermissionDownload(updateRequest.getPermissions().isCanDownload());
|
||||
user.getPermissions().setPermissionEditMetadata(updateRequest.getPermissions().isCanEditMetadata());
|
||||
user.getPermissions().setPermissionManipulateLibrary(updateRequest.getPermissions().isCanManipulateLibrary());
|
||||
user.getPermissions().setPermissionManageLibrary(updateRequest.getPermissions().isCanManageLibrary());
|
||||
user.getPermissions().setPermissionEmailBook(updateRequest.getPermissions().isCanEmailBook());
|
||||
user.getPermissions().setPermissionDeleteBook(updateRequest.getPermissions().isCanDeleteBook());
|
||||
user.getPermissions().setPermissionAccessOpds(updateRequest.getPermissions().isCanAccessOpds());
|
||||
user.getPermissions().setPermissionSyncKoreader(updateRequest.getPermissions().isCanSyncKoReader());
|
||||
user.getPermissions().setPermissionSyncKobo(updateRequest.getPermissions().isCanSyncKobo());
|
||||
user.getPermissions().setPermissionManageMetadataConfig(updateRequest.getPermissions().isCanManageMetadataConfig());
|
||||
user.getPermissions().setPermissionAccessBookdrop(updateRequest.getPermissions().isCanAccessBookdrop());
|
||||
user.getPermissions().setPermissionAccessLibraryStats(updateRequest.getPermissions().isCanAccessLibraryStats());
|
||||
user.getPermissions().setPermissionAccessUserStats(updateRequest.getPermissions().isCanAccessUserStats());
|
||||
user.getPermissions().setPermissionAccessTaskManager(updateRequest.getPermissions().isCanAccessTaskManager());
|
||||
user.getPermissions().setPermissionManageGlobalPreferences(updateRequest.getPermissions().isCanManageGlobalPreferences());
|
||||
user.getPermissions().setPermissionManageIcons(updateRequest.getPermissions().isCanManageIcons());
|
||||
}
|
||||
|
||||
if (updateRequest.getAssignedLibraries() != null && getMyself().getPermissions().isAdmin()) {
|
||||
@@ -92,9 +99,14 @@ public class UserService {
|
||||
|
||||
public void changePassword(ChangePasswordRequest changePasswordRequest) {
|
||||
BookLoreUser bookLoreUser = authenticationService.getAuthenticatedUser();
|
||||
|
||||
BookLoreUserEntity bookLoreUserEntity = userRepository.findById(bookLoreUser.getId())
|
||||
.orElseThrow(() -> ApiError.USER_NOT_FOUND.createException(bookLoreUser.getId()));
|
||||
|
||||
if(bookLoreUserEntity.getPermissions().isPermissionDemoUser()) {
|
||||
throw ApiError.DEMO_USER_PASSWORD_CHANGE_NOT_ALLOWED.createException();
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(changePasswordRequest.getCurrentPassword(), bookLoreUserEntity.getPasswordHash())) {
|
||||
throw ApiError.PASSWORD_INCORRECT.createException();
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import static com.adityachandel.booklore.model.enums.PermissionType.ADMIN;
|
||||
import static com.adityachandel.booklore.model.enums.PermissionType.MANIPULATE_LIBRARY;
|
||||
import static com.adityachandel.booklore.model.enums.PermissionType.MANAGE_LIBRARY;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -52,7 +52,7 @@ public class BookFilePersistenceService {
|
||||
} else {
|
||||
log.info("[FILE_CREATE] Book with hash '{}' already exists at same path. Skipping update.", currentHash);
|
||||
}
|
||||
notificationService.sendMessageToPermissions(Topic.BOOK_ADD, bookMapper.toBookWithDescription(book, false), Set.of(ADMIN, MANIPULATE_LIBRARY));
|
||||
notificationService.sendMessageToPermissions(Topic.BOOK_ADD, bookMapper.toBookWithDescription(book, false), Set.of(ADMIN, MANAGE_LIBRARY));
|
||||
}
|
||||
|
||||
String findMatchingLibraryPath(LibraryEntity libraryEntity, Path filePath) {
|
||||
|
||||
@@ -23,7 +23,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.adityachandel.booklore.model.enums.PermissionType.ADMIN;
|
||||
import static com.adityachandel.booklore.model.enums.PermissionType.MANIPULATE_LIBRARY;
|
||||
import static com.adityachandel.booklore.model.enums.PermissionType.MANAGE_LIBRARY;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -43,7 +43,7 @@ public class BookFileTransactionalHandler {
|
||||
String fileName = path.getFileName().toString();
|
||||
String libraryPath = bookFilePersistenceService.findMatchingLibraryPath(libraryEntity, path);
|
||||
|
||||
notificationService.sendMessageToPermissions(Topic.LOG, LogNotification.info("Started processing file: " + filePath), Set.of(ADMIN, MANIPULATE_LIBRARY));
|
||||
notificationService.sendMessageToPermissions(Topic.LOG, LogNotification.info("Started processing file: " + filePath), Set.of(ADMIN, MANAGE_LIBRARY));
|
||||
|
||||
LibraryPathEntity libraryPathEntity = bookFilePersistenceService.getLibraryPathEntityForFile(libraryEntity, libraryPath);
|
||||
|
||||
@@ -59,7 +59,7 @@ public class BookFileTransactionalHandler {
|
||||
|
||||
libraryProcessingService.processLibraryFiles(List.of(libraryFile), libraryEntity);
|
||||
|
||||
notificationService.sendMessageToPermissions(Topic.LOG, LogNotification.info("Finished processing file: " + filePath), Set.of(ADMIN, MANIPULATE_LIBRARY));
|
||||
notificationService.sendMessageToPermissions(Topic.LOG, LogNotification.info("Finished processing file: " + filePath), Set.of(ADMIN, MANAGE_LIBRARY));
|
||||
log.info("[CREATE] Completed processing for file '{}'", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ public class LibraryFileEventProcessor {
|
||||
book.setDeleted(true);
|
||||
bookFilePersistenceService.save(book);
|
||||
notificationService.sendMessageToPermissions(Topic.BOOKS_REMOVE, Set.of(book.getId()),
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANIPULATE_LIBRARY));
|
||||
Set.of(PermissionType.ADMIN, PermissionType.MANAGE_LIBRARY));
|
||||
log.info("[MARKED_DELETED] Book '{}' marked as deleted", fileName);
|
||||
}, () -> log.warn("[NOT_FOUND] Book for deleted path '{}' not found", path));
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.adityachandel.booklore.util;
|
||||
|
||||
import com.adityachandel.booklore.model.dto.BookLoreUser;
|
||||
import com.adityachandel.booklore.model.entity.UserPermissionsEntity;
|
||||
import com.adityachandel.booklore.model.enums.PermissionType;
|
||||
import lombok.experimental.UtilityClass;
|
||||
@@ -9,16 +10,47 @@ public class UserPermissionUtils {
|
||||
|
||||
public static boolean hasPermission(UserPermissionsEntity perms, PermissionType type) {
|
||||
return switch (type) {
|
||||
case ADMIN -> perms.isPermissionAdmin();
|
||||
case UPLOAD -> perms.isPermissionUpload();
|
||||
case DOWNLOAD -> perms.isPermissionDownload();
|
||||
case EDIT_METADATA -> perms.isPermissionEditMetadata();
|
||||
case MANIPULATE_LIBRARY -> perms.isPermissionManipulateLibrary();
|
||||
case MANAGE_LIBRARY -> perms.isPermissionManageLibrary();
|
||||
case EMAIL_BOOK -> perms.isPermissionEmailBook();
|
||||
case DELETE_BOOK -> perms.isPermissionDeleteBook();
|
||||
case ACCESS_OPDS -> perms.isPermissionAccessOpds();
|
||||
case SYNC_KOREADER -> perms.isPermissionSyncKoreader();
|
||||
case SYNC_KOBO -> perms.isPermissionSyncKobo();
|
||||
case ADMIN -> perms.isPermissionAdmin();
|
||||
case MANAGE_METADATA_CONFIG -> perms.isPermissionManageMetadataConfig();
|
||||
case ACCESS_BOOKDROP -> perms.isPermissionAccessBookdrop();
|
||||
case ACCESS_LIBRARY_STATS -> perms.isPermissionAccessLibraryStats();
|
||||
case ACCESS_USER_STATS -> perms.isPermissionAccessUserStats();
|
||||
case ACCESS_TASK_MANAGER -> perms.isPermissionAccessTaskManager();
|
||||
case MANAGE_ICONS -> perms.isPermissionManageIcons();
|
||||
case MANAGE_GLOBAL_PREFERENCES -> perms.isPermissionManageGlobalPreferences();
|
||||
case DEMO_USER -> perms.isPermissionDemoUser();
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean hasPermission(BookLoreUser.UserPermissions perms, PermissionType type) {
|
||||
return switch (type) {
|
||||
case ADMIN -> perms.isAdmin();
|
||||
case UPLOAD -> perms.isCanUpload();
|
||||
case DOWNLOAD -> perms.isCanDownload();
|
||||
case EDIT_METADATA -> perms.isCanEditMetadata();
|
||||
case MANAGE_LIBRARY -> perms.isCanManageLibrary();
|
||||
case EMAIL_BOOK -> perms.isCanEmailBook();
|
||||
case DELETE_BOOK -> perms.isCanDeleteBook();
|
||||
case ACCESS_OPDS -> perms.isCanAccessOpds();
|
||||
case SYNC_KOREADER -> perms.isCanSyncKoReader();
|
||||
case SYNC_KOBO -> perms.isCanSyncKobo();
|
||||
case MANAGE_METADATA_CONFIG -> perms.isCanManageMetadataConfig();
|
||||
case ACCESS_BOOKDROP -> perms.isCanAccessBookdrop();
|
||||
case ACCESS_LIBRARY_STATS -> perms.isCanAccessLibraryStats();
|
||||
case ACCESS_USER_STATS -> perms.isCanAccessUserStats();
|
||||
case ACCESS_TASK_MANAGER -> perms.isCanAccessTaskManager();
|
||||
case MANAGE_ICONS -> perms.isCanManageIcons();
|
||||
case MANAGE_GLOBAL_PREFERENCES -> perms.isCanManageGlobalPreferences();
|
||||
case DEMO_USER -> perms.isDemoUser();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
ALTER TABLE user_permissions
|
||||
ADD COLUMN IF NOT EXISTS permission_manage_metadata_config BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS permission_access_bookdrop BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS permission_access_library_stats BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS permission_access_user_stats BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS permission_access_task_manager BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS permission_manage_global_preferences BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS permission_manage_icons BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS permission_demo_user BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- Set all new permissions to TRUE for admin users
|
||||
UPDATE user_permissions up
|
||||
SET up.permission_manage_metadata_config = TRUE
|
||||
WHERE up.permission_admin = TRUE;
|
||||
|
||||
UPDATE user_permissions up
|
||||
SET up.permission_access_bookdrop = TRUE
|
||||
WHERE up.permission_admin = TRUE;
|
||||
|
||||
UPDATE user_permissions up
|
||||
SET up.permission_access_library_stats = TRUE
|
||||
WHERE up.permission_admin = TRUE;
|
||||
|
||||
UPDATE user_permissions up
|
||||
SET up.permission_access_user_stats = TRUE
|
||||
WHERE up.permission_admin = TRUE;
|
||||
|
||||
UPDATE user_permissions up
|
||||
SET up.permission_access_task_manager = TRUE
|
||||
WHERE up.permission_admin = TRUE;
|
||||
|
||||
UPDATE user_permissions up
|
||||
SET up.permission_manage_global_preferences = TRUE
|
||||
WHERE up.permission_admin = TRUE;
|
||||
|
||||
UPDATE user_permissions up
|
||||
SET up.permission_manage_icons = TRUE
|
||||
WHERE up.permission_admin = TRUE;
|
||||
@@ -86,10 +86,10 @@ class BookReviewServiceTest {
|
||||
.build();
|
||||
}
|
||||
|
||||
private BookLoreUser createUser(boolean isAdmin, boolean canManipulateLibrary) {
|
||||
private BookLoreUser createUser(boolean isAdmin, boolean canManageLibrary) {
|
||||
BookLoreUser.UserPermissions permissions = new BookLoreUser.UserPermissions();
|
||||
permissions.setAdmin(isAdmin);
|
||||
permissions.setCanManipulateLibrary(canManipulateLibrary);
|
||||
permissions.setCanManageLibrary(canManageLibrary);
|
||||
|
||||
BookLoreUser user = new BookLoreUser();
|
||||
user.setPermissions(permissions);
|
||||
|
||||
@@ -29,12 +29,20 @@ class UserPermissionUtilsTest {
|
||||
.permissionUpload(false)
|
||||
.permissionDownload(false)
|
||||
.permissionEditMetadata(false)
|
||||
.permissionManipulateLibrary(false)
|
||||
.permissionManageLibrary(false)
|
||||
.permissionEmailBook(false)
|
||||
.permissionDeleteBook(false)
|
||||
.permissionAccessOpds(false)
|
||||
.permissionSyncKoreader(false)
|
||||
.permissionSyncKobo(false)
|
||||
.permissionManageMetadataConfig(false)
|
||||
.permissionAccessBookdrop(false)
|
||||
.permissionAccessLibraryStats(false)
|
||||
.permissionAccessUserStats(false)
|
||||
.permissionAccessTaskManager(false)
|
||||
.permissionManageGlobalPreferences(false)
|
||||
.permissionManageIcons(false)
|
||||
.permissionDemoUser(false)
|
||||
.permissionAdmin(false)
|
||||
.build();
|
||||
|
||||
@@ -49,12 +57,20 @@ class UserPermissionUtilsTest {
|
||||
.permissionUpload(true)
|
||||
.permissionDownload(true)
|
||||
.permissionEditMetadata(true)
|
||||
.permissionManipulateLibrary(true)
|
||||
.permissionManageLibrary(true)
|
||||
.permissionEmailBook(true)
|
||||
.permissionDeleteBook(true)
|
||||
.permissionAccessOpds(true)
|
||||
.permissionSyncKoreader(true)
|
||||
.permissionSyncKobo(true)
|
||||
.permissionManageMetadataConfig(true)
|
||||
.permissionAccessBookdrop(true)
|
||||
.permissionAccessLibraryStats(true)
|
||||
.permissionAccessUserStats(true)
|
||||
.permissionAccessTaskManager(true)
|
||||
.permissionManageGlobalPreferences(true)
|
||||
.permissionManageIcons(true)
|
||||
.permissionDemoUser(true)
|
||||
.permissionAdmin(true)
|
||||
.build();
|
||||
|
||||
@@ -68,24 +84,40 @@ class UserPermissionUtilsTest {
|
||||
.permissionUpload(false)
|
||||
.permissionDownload(false)
|
||||
.permissionEditMetadata(false)
|
||||
.permissionManipulateLibrary(false)
|
||||
.permissionManageLibrary(false)
|
||||
.permissionEmailBook(false)
|
||||
.permissionDeleteBook(false)
|
||||
.permissionAccessOpds(false)
|
||||
.permissionSyncKoreader(false)
|
||||
.permissionSyncKobo(false)
|
||||
.permissionManageMetadataConfig(false)
|
||||
.permissionAccessBookdrop(false)
|
||||
.permissionAccessLibraryStats(false)
|
||||
.permissionAccessUserStats(false)
|
||||
.permissionAccessTaskManager(false)
|
||||
.permissionManageGlobalPreferences(false)
|
||||
.permissionManageIcons(false)
|
||||
.permissionDemoUser(false)
|
||||
.permissionAdmin(false);
|
||||
|
||||
switch (permissionType) {
|
||||
case UPLOAD -> builder.permissionUpload(value);
|
||||
case DOWNLOAD -> builder.permissionDownload(value);
|
||||
case EDIT_METADATA -> builder.permissionEditMetadata(value);
|
||||
case MANIPULATE_LIBRARY -> builder.permissionManipulateLibrary(value);
|
||||
case MANAGE_LIBRARY -> builder.permissionManageLibrary(value);
|
||||
case EMAIL_BOOK -> builder.permissionEmailBook(value);
|
||||
case DELETE_BOOK -> builder.permissionDeleteBook(value);
|
||||
case ACCESS_OPDS -> builder.permissionAccessOpds(value);
|
||||
case SYNC_KOREADER -> builder.permissionSyncKoreader(value);
|
||||
case SYNC_KOBO -> builder.permissionSyncKobo(value);
|
||||
case MANAGE_METADATA_CONFIG -> builder.permissionManageMetadataConfig(value);
|
||||
case ACCESS_BOOKDROP -> builder.permissionAccessBookdrop(value);
|
||||
case ACCESS_LIBRARY_STATS -> builder.permissionAccessLibraryStats(value);
|
||||
case ACCESS_USER_STATS -> builder.permissionAccessUserStats(value);
|
||||
case ACCESS_TASK_MANAGER -> builder.permissionAccessTaskManager(value);
|
||||
case MANAGE_GLOBAL_PREFERENCES -> builder.permissionManageGlobalPreferences(value);
|
||||
case MANAGE_ICONS -> builder.permissionManageIcons(value);
|
||||
case DEMO_USER -> builder.permissionDemoUser(value);
|
||||
case ADMIN -> builder.permissionAdmin(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ import {BookdropFileReviewComponent} from './features/bookdrop/component/bookdro
|
||||
import {ManageLibraryGuard} from './core/security/guards/manage-library.guard';
|
||||
import {LoginGuard} from './shared/components/setup/login.guard';
|
||||
import {UserStatsComponent} from './features/stats/component/user-stats/user-stats.component';
|
||||
import {BookdropGuard} from './core/security/guards/bookdrop.guard';
|
||||
import {LibraryStatsGuard} from './core/security/guards/library-stats.guard';
|
||||
import {UserStatsGuard} from './core/security/guards/user-stats.guard';
|
||||
import {EditMetadataGuard} from './core/security/guards/edit-metdata.guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -49,10 +53,10 @@ export const routes: Routes = [
|
||||
{path: 'series/:seriesName', component: SeriesPageComponent, canActivate: [AuthGuard]},
|
||||
{path: 'magic-shelf/:magicShelfId/books', component: BookBrowserComponent, canActivate: [AuthGuard]},
|
||||
{path: 'book/:bookId', component: BookMetadataCenterComponent, canActivate: [AuthGuard]},
|
||||
{path: 'bookdrop', component: BookdropFileReviewComponent, canActivate: [ManageLibraryGuard]},
|
||||
{path: 'metadata-manager', component: MetadataManagerComponent, canActivate: [ManageLibraryGuard]},
|
||||
{path: 'library-stats', component: StatsComponent, canActivate: [AuthGuard]},
|
||||
{path: 'reading-stats', component: UserStatsComponent, canActivate: [AuthGuard]},
|
||||
{path: 'bookdrop', component: BookdropFileReviewComponent, canActivate: [BookdropGuard]},
|
||||
{path: 'metadata-manager', component: MetadataManagerComponent, canActivate: [EditMetadataGuard]},
|
||||
{path: 'library-stats', component: StatsComponent, canActivate: [LibraryStatsGuard]},
|
||||
{path: 'reading-stats', component: UserStatsComponent, canActivate: [UserStatsGuard]},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
20
booklore-ui/src/app/core/security/guards/bookdrop.guard.ts
Normal file
20
booklore-ui/src/app/core/security/guards/bookdrop.guard.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
export const BookdropGuard: CanActivateFn = () => {
|
||||
const userService = inject(UserService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canAccessBookdrop)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
export const EditMetadataGuard: CanActivateFn = () => {
|
||||
const userService = inject(UserService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canEditMetadata)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
export const LibraryStatsGuard: CanActivateFn = () => {
|
||||
const userService = inject(UserService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canAccessLibraryStats)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,7 @@ export const ManageLibraryGuard: CanActivateFn = () => {
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canManipulateLibrary)) {
|
||||
if (user && (user.permissions.admin || user.permissions.canManageLibrary)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
|
||||
20
booklore-ui/src/app/core/security/guards/user-stats.guard.ts
Normal file
20
booklore-ui/src/app/core/security/guards/user-stats.guard.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
export const UserStatsGuard: CanActivateFn = () => {
|
||||
const userService = inject(UserService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canAccessUserStats)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (entityType !== EntityType.ALL_BOOKS && entityType !== EntityType.UNSHELVED &&
|
||||
(userState.user!.permissions.admin || userState.user!.permissions.canManipulateLibrary)) {
|
||||
(userState.user!.permissions.admin || userState.user!.permissions.canManageLibrary)) {
|
||||
<div class="ml-2 flex-shrink-0">
|
||||
<p-button
|
||||
icon="pi pi-ellipsis-v"
|
||||
|
||||
@@ -39,7 +39,7 @@ export class BookdropFileService implements OnDestroy {
|
||||
)
|
||||
.subscribe(state => {
|
||||
const user = state.user!;
|
||||
if (user.permissions.admin || user.permissions.canManipulateLibrary) {
|
||||
if (user.permissions.admin || user.permissions.canAccessBookdrop) {
|
||||
this.authService.token$
|
||||
.pipe(filter(t => !!t), take(1))
|
||||
.subscribe(() => this.refresh());
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="dashboard-no-library">
|
||||
@if ((userService.userState$ | async)?.user?.permissions; as permissions) {
|
||||
<div>
|
||||
@if (permissions.admin || permissions.canManipulateLibrary) {
|
||||
@if (permissions.admin || permissions.canManageLibrary) {
|
||||
<div>
|
||||
<h1 class="no-library-header">
|
||||
Welcome to BookLore!<br>
|
||||
|
||||
@@ -9,16 +9,20 @@
|
||||
<p-tab [value]="SettingsTab.ViewPreferences">
|
||||
<i class="pi pi-desktop"></i> View
|
||||
</p-tab>
|
||||
@if (userState.user.permissions.admin) {
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageMetadataConfig) {
|
||||
<p-tab [value]="SettingsTab.MetadataSettings">
|
||||
<i class="pi pi-sliders-h"></i> Metadata 1
|
||||
</p-tab>
|
||||
<p-tab [value]="SettingsTab.LibraryMetadataSettings">
|
||||
<i class="pi pi-database"></i> Metadata 2
|
||||
</p-tab>
|
||||
}
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageGlobalPreferences) {
|
||||
<p-tab [value]="SettingsTab.ApplicationSettings">
|
||||
<i class="pi pi-cog"></i> Application
|
||||
</p-tab>
|
||||
}
|
||||
@if (userState.user.permissions.admin) {
|
||||
<p-tab [value]="SettingsTab.UserManagement">
|
||||
<i class="pi pi-users"></i> Users
|
||||
</p-tab>
|
||||
@@ -26,14 +30,17 @@
|
||||
<p-tab [value]="SettingsTab.EmailSettingsV2">
|
||||
<i class="pi pi-envelope"></i> Email
|
||||
</p-tab>
|
||||
|
||||
@if (userState.user.permissions.admin) {
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageMetadataConfig) {
|
||||
<p-tab [value]="SettingsTab.NamingPattern">
|
||||
<i class="pi pi-sitemap"></i> Patterns
|
||||
</p-tab>
|
||||
}
|
||||
@if (userState.user.permissions.admin) {
|
||||
<p-tab [value]="SettingsTab.AuthenticationSettings">
|
||||
<i class="pi pi-lock"></i> Authentication
|
||||
</p-tab>
|
||||
}
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canAccessTaskManager) {
|
||||
<p-tab [value]="SettingsTab.Tasks">
|
||||
<i class="pi pi-list-check"></i> Tasks
|
||||
</p-tab>
|
||||
@@ -52,16 +59,20 @@
|
||||
<p-tabpanel [value]="SettingsTab.ViewPreferences">
|
||||
<app-view-preferences-parent></app-view-preferences-parent>
|
||||
</p-tabpanel>
|
||||
@if (userState.user.permissions.admin) {
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageMetadataConfig) {
|
||||
<p-tabpanel [value]="SettingsTab.MetadataSettings">
|
||||
<app-metadata-settings-component></app-metadata-settings-component>
|
||||
</p-tabpanel>
|
||||
<p-tabpanel [value]="SettingsTab.LibraryMetadataSettings">
|
||||
<app-library-metadata-settings-component></app-library-metadata-settings-component>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageGlobalPreferences) {
|
||||
<p-tabpanel [value]="SettingsTab.ApplicationSettings">
|
||||
<app-global-preferences></app-global-preferences>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (userState.user.permissions.admin) {
|
||||
<p-tabpanel [value]="SettingsTab.UserManagement">
|
||||
<app-user-management></app-user-management>
|
||||
</p-tabpanel>
|
||||
@@ -69,13 +80,17 @@
|
||||
<p-tabpanel [value]="SettingsTab.EmailSettingsV2">
|
||||
<app-email-v2></app-email-v2>
|
||||
</p-tabpanel>
|
||||
@if (userState.user.permissions.admin) {
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageMetadataConfig) {
|
||||
<p-tabpanel [value]="SettingsTab.NamingPattern">
|
||||
<app-file-naming-pattern></app-file-naming-pattern>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (userState.user.permissions.admin) {
|
||||
<p-tabpanel [value]="SettingsTab.AuthenticationSettings">
|
||||
<app-authentication-settings></app-authentication-settings>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canAccessTaskManager) {
|
||||
<p-tabpanel [value]="SettingsTab.Tasks">
|
||||
<app-task-management></app-task-management>
|
||||
</p-tabpanel>
|
||||
|
||||
@@ -29,65 +29,51 @@
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<p-table [value]="users" [scrollable]="true" scrollHeight="flex">
|
||||
<p-table
|
||||
[value]="users"
|
||||
[scrollable]="true"
|
||||
scrollHeight="flex"
|
||||
dataKey="id">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>Username</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<th style="width: 40px"></th>
|
||||
<th style="width: 100px; text-align: center">
|
||||
<div class="header-content" style="justify-content: center">
|
||||
<i class="pi pi-tag"></i>
|
||||
<span>Type</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="full-name-column">
|
||||
<th style="min-width: 180px">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-id-card"></i>
|
||||
<span>Full Name</span>
|
||||
<i class="pi pi-user"></i>
|
||||
<span>User</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="email-column">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>Email</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="libraries-column">
|
||||
<th style="min-width: 200px">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-book"></i>
|
||||
<span>Assigned Libraries</span>
|
||||
<span>Libraries</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="permission-header">Admin</th>
|
||||
<th class="permission-header">Upload</th>
|
||||
<th class="permission-header">Download</th>
|
||||
<th class="permission-header">Manage Metadata</th>
|
||||
<th class="permission-header">Manage Library</th>
|
||||
<th class="permission-header">Email Books</th>
|
||||
<th class="permission-header">Delete Books</th>
|
||||
<th class="permission-header">Access OPDS</th>
|
||||
<th class="permission-header">KOReader Sync</th>
|
||||
<th class="permission-header">Kobo Sync</th>
|
||||
<th class="actions-header">
|
||||
<div class="header-content">
|
||||
<th style="width: 120px; text-align: center" pTooltip="Admin permissions" tooltipPosition="top">
|
||||
<i class="pi pi-shield"></i>
|
||||
</th>
|
||||
<th style="width: 120px; text-align: center" pTooltip="Book Management permissions" tooltipPosition="top">
|
||||
<i class="pi pi-book"></i>
|
||||
</th>
|
||||
<th style="width: 120px; text-align: center" pTooltip="Device Sync permissions" tooltipPosition="top">
|
||||
<i class="pi pi-mobile"></i>
|
||||
</th>
|
||||
<th style="width: 120px; text-align: center" pTooltip="System Access permissions" tooltipPosition="top">
|
||||
<i class="pi pi-eye"></i>
|
||||
</th>
|
||||
<th style="width: 120px; text-align: center" pTooltip="System Configuration permissions" tooltipPosition="top">
|
||||
<i class="pi pi-cog"></i>
|
||||
<span>Edit</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="actions-header">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-key"></i>
|
||||
<span>Password</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="actions-header">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-trash"></i>
|
||||
<span>Delete</span>
|
||||
<th style="width: 180px; text-align: center">
|
||||
<div class="header-content" style="justify-content: center">
|
||||
<i class="pi pi-ellipsis-h"></i>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -95,36 +81,31 @@
|
||||
|
||||
<ng-template pTemplate="body" let-user>
|
||||
<tr>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
pButton
|
||||
class="p-button-text p-button-rounded p-button-plain"
|
||||
(click)="toggleRowExpansion(user)">
|
||||
<i [class]="isRowExpanded(user) ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="user-type-badge" [attr.data-type]="user.provisioningMethod || 'LOCAL'">
|
||||
{{ (user.provisioningMethod || 'LOCAL') | lowercase | titlecase }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="user-details">
|
||||
<span class="username">{{ user.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="user-type-badge">
|
||||
{{ (user.provisioningMethod || 'LOCAL') | lowercase | titlecase }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="full-name-column">
|
||||
@if (user.isEditing) {
|
||||
<input type="text" [(ngModel)]="user.name" class="p-inputtext w-full" size="small"/>
|
||||
}
|
||||
@if (!user.isEditing) {
|
||||
<span>{{ user.name }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="email-column">
|
||||
@if (user.isEditing) {
|
||||
<input type="email" [(ngModel)]="user.email" class="p-inputtext w-full" size="small"/>
|
||||
}
|
||||
@if (!user.isEditing) {
|
||||
<span>{{ user.email }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="libraries-column">
|
||||
@if (user.isEditing) {
|
||||
<p-multiSelect
|
||||
[options]="allLibraries"
|
||||
@@ -133,102 +114,244 @@
|
||||
[(ngModel)]="editingLibraryIds"
|
||||
placeholder="Select Libraries"
|
||||
appendTo="body"
|
||||
[style]="{'width': '100%'}"
|
||||
size="small">
|
||||
</p-multiSelect>
|
||||
}
|
||||
@if (!user.isEditing) {
|
||||
<span class="library-names">{{ user.libraryNames }}</span>
|
||||
} @else {
|
||||
<span class="library-names">{{ user.libraryNames || 'None' }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.admin" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-indicator admin-indicator" [class.active]="user.permissions.admin">
|
||||
@if (user.permissions.admin) {
|
||||
<i class="pi pi-shield" pTooltip="Administrator"></i>
|
||||
} @else {
|
||||
<i class="pi pi-minus" pTooltip="Not Administrator"></i>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canUpload" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-summary" [attr.data-count]="getBookManagementPermissionsCount(user)" [attr.data-total]="6">
|
||||
<span class="permission-count">
|
||||
{{ getBookManagementPermissionsCount(user) }}/6
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canDownload" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-summary" [attr.data-count]="getDeviceSyncPermissionsCount(user)" [attr.data-total]="3">
|
||||
<span class="permission-count">
|
||||
{{ getDeviceSyncPermissionsCount(user) }}/3
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canEditMetadata" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-summary" [attr.data-count]="getSystemAccessPermissionsCount(user)" [attr.data-total]="3">
|
||||
<span class="permission-count">
|
||||
{{ getSystemAccessPermissionsCount(user) }}/3
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManipulateLibrary" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-summary" [attr.data-count]="getSystemConfigPermissionsCount(user)" [attr.data-total]="4">
|
||||
<span class="permission-count">
|
||||
{{ getSystemConfigPermissionsCount(user) }}/4
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canEmailBook" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canDeleteBook" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessOpds" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canSyncKoReader" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canSyncKobo" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<td>
|
||||
<div class="actions-group">
|
||||
@if (!user.isEditing) {
|
||||
<p-button
|
||||
icon="pi pi-pencil"
|
||||
severity="info"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="toggleEdit(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Edit user">
|
||||
</p-button>
|
||||
}
|
||||
@if (user.isEditing) {
|
||||
<div class="flex gap-1">
|
||||
<p-button
|
||||
icon="pi pi-check"
|
||||
severity="success"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="saveUser(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Save changes">
|
||||
</p-button>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
severity="danger"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="toggleEdit(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Cancel">
|
||||
</p-button>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
@if (!user.isEditing) {
|
||||
<p-button
|
||||
icon="pi pi-key"
|
||||
severity="warn"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="openChangePasswordDialog(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Change password">
|
||||
</p-button>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<p-button
|
||||
[disabled]="user.id === currentUser?.id"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="deleteUser(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Delete user">
|
||||
</p-button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@if (isRowExpanded(user)) {
|
||||
<tr>
|
||||
<td colspan="10">
|
||||
<div class="expanded-content">
|
||||
<div class="expanded-section">
|
||||
<h4 class="expanded-title">
|
||||
<i class="pi pi-id-card"></i>
|
||||
User Information
|
||||
</h4>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Full Name</label>
|
||||
@if (user.isEditing) {
|
||||
<input type="text" [(ngModel)]="user.name" class="p-inputtext p-component p-inputtext-sm"/>
|
||||
} @else {
|
||||
<span>{{ user.name || 'N/A' }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Email</label>
|
||||
@if (user.isEditing) {
|
||||
<input type="email" [(ngModel)]="user.email" class="p-inputtext p-component p-inputtext-sm"/>
|
||||
} @else {
|
||||
<span>{{ user.email || 'N/A' }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="expanded-section">
|
||||
<h4 class="expanded-title">
|
||||
<i class="pi pi-lock"></i>
|
||||
Permissions
|
||||
</h4>
|
||||
<div class="permissions-grid">
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-book"></i> Book Management</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canUpload" [disabled]="isPermissionDisabled(user)" inputId="upload-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'upload-'+user.id">Upload Books</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canDownload" [disabled]="isPermissionDisabled(user)" inputId="download-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'download-'+user.id">Download Books</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canDeleteBook" [disabled]="isPermissionDisabled(user)" inputId="delete-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'delete-'+user.id">Delete Books</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canEditMetadata" [disabled]="isPermissionDisabled(user)" inputId="metadata-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'metadata-'+user.id">Edit Metadata</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManageLibrary" [disabled]="isPermissionDisabled(user)" inputId="library-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'library-'+user.id">Manage Library</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canEmailBook" [disabled]="isPermissionDisabled(user)" inputId="email-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'email-'+user.id">Email Books</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-mobile"></i> Device Sync</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canSyncKoReader" [disabled]="isPermissionDisabled(user)" inputId="koreader-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'koreader-'+user.id">KOReader Sync</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canSyncKobo" [disabled]="isPermissionDisabled(user)" inputId="kobo-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'kobo-'+user.id">Kobo Sync</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessOpds" [disabled]="isPermissionDisabled(user)" inputId="opds-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'opds-'+user.id">OPDS Feed Access</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-eye"></i> System Access</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessBookdrop" [disabled]="isPermissionDisabled(user)" inputId="bookdrop-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'bookdrop-'+user.id">Access Bookdrop</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessLibraryStats" [disabled]="isPermissionDisabled(user)" inputId="stats-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'stats-'+user.id">View Library Statistics</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessUserStats" [disabled]="isPermissionDisabled(user)" inputId="userstats-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'userstats-'+user.id">View User Reading Statistics</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-cog"></i> System Configuration</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManageMetadataConfig" [disabled]="isPermissionDisabled(user)" inputId="metadataconfig-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'metadataconfig-'+user.id">Manage Metadata Configuration</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManageGlobalPreferences" [disabled]="isPermissionDisabled(user)" inputId="preferences-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'preferences-'+user.id">Manage Application Preferences</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessTaskManager" [disabled]="isPermissionDisabled(user)" inputId="taskmanager-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'taskmanager-'+user.id">Access Task Manager</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManageIcons" [disabled]="isPermissionDisabled(user)" inputId="icons-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'icons-'+user.id">Manage Icons</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-shield"></i> Administration</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.admin" (onChange)="onAdminCheckboxChange(user)" [disabled]="!user.isEditing" inputId="admin-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'admin-'+user.id">Full Administrator Access</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
|
||||
@@ -137,12 +137,12 @@
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--p-primary-color);
|
||||
color: white;
|
||||
@@ -151,93 +151,292 @@
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
}
|
||||
|
||||
.user-type-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid var(--p-primary-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--p-text-color);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border: 1px solid;
|
||||
border-radius: 12px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
|
||||
&[data-type="LOCAL"] {
|
||||
color: #3b82f6;
|
||||
background: color-mix(in srgb, #3b82f6 15%, transparent);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&[data-type="OIDC"] {
|
||||
color: #8b5cf6;
|
||||
background: color-mix(in srgb, #8b5cf6 15%, transparent);
|
||||
border-color: #8b5cf6;
|
||||
}
|
||||
|
||||
&:not([data-type="LOCAL"]):not([data-type="OIDC"]) {
|
||||
color: #6b7280;
|
||||
background: color-mix(in srgb, #6b7280 15%, transparent);
|
||||
border-color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.library-names {
|
||||
.permission-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--card-background);
|
||||
color: var(--text-color-secondary);
|
||||
transition: all 0.2s;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&.active {
|
||||
background: color-mix(in srgb, var(--primary-color) 20%, var(--card-background));
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
.pi {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-indicator {
|
||||
&.active {
|
||||
background: color-mix(in srgb, #10b981 20%, var(--card-background));
|
||||
color: #10b981;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
background: color-mix(in srgb, #6b7280 15%, var(--card-background));
|
||||
color: #6b7280;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem 0.875rem;
|
||||
border-radius: 16px;
|
||||
background: var(--card-background);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s;
|
||||
|
||||
&[data-count="0"] {
|
||||
background: color-mix(in srgb, #6b7280 20%, var(--card-background));
|
||||
border-color: #6b7280;
|
||||
|
||||
.permission-count {
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-total="6"][data-count="1"],
|
||||
&[data-total="6"][data-count="2"],
|
||||
&[data-total="3"][data-count="1"],
|
||||
&[data-total="2"][data-count="1"],
|
||||
&[data-total="4"][data-count="1"] {
|
||||
background: color-mix(in srgb, #ef4444 20%, var(--card-background));
|
||||
border-color: #ef4444;
|
||||
|
||||
.permission-count {
|
||||
color: #ef4444;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-total="6"][data-count="3"],
|
||||
&[data-total="6"][data-count="4"],
|
||||
&[data-total="3"][data-count="2"],
|
||||
&[data-total="4"][data-count="2"] {
|
||||
background: color-mix(in srgb, #f59e0b 20%, var(--card-background));
|
||||
border-color: #f59e0b;
|
||||
|
||||
.permission-count {
|
||||
color: #f59e0b;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-total="6"][data-count="5"],
|
||||
&[data-total="4"][data-count="3"] {
|
||||
background: color-mix(in srgb, #3b82f6 20%, var(--card-background));
|
||||
border-color: #3b82f6;
|
||||
|
||||
.permission-count {
|
||||
color: #3b82f6;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-total="6"][data-count="6"],
|
||||
&[data-total="3"][data-count="3"],
|
||||
&[data-total="2"][data-count="2"],
|
||||
&[data-total="4"][data-count="4"] {
|
||||
background: color-mix(in srgb, #10b981 20%, var(--card-background));
|
||||
border-color: #10b981;
|
||||
|
||||
.permission-count {
|
||||
color: #10b981;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.permission-count {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
text-align: center;
|
||||
.actions-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.user-dialog {
|
||||
|
||||
.p-dialog-content {
|
||||
.expanded-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.p-dialog-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-form {
|
||||
background: var(--overlay-background);
|
||||
border-radius: 8px;
|
||||
margin: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
.expanded-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.expanded-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.pi {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
.permissions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.permission-group {
|
||||
h5 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--p-red-50);
|
||||
border: 1px solid var(--p-red-200);
|
||||
border-radius: 4px;
|
||||
color: var(--p-red-700);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 0.75rem 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
|
||||
.pi {
|
||||
color: var(--p-red-500);
|
||||
color: var(--primary-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
.permission-items {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.full-name-column {
|
||||
min-width: 150px;
|
||||
width: 150px;
|
||||
.permission-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
p-checkbox {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.email-column {
|
||||
min-width: 150px;
|
||||
width: 200px;
|
||||
.library-names {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.libraries-column {
|
||||
min-width: 200px;
|
||||
width: 250px;
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Button} from 'primeng/button';
|
||||
import {Button, ButtonDirective} from 'primeng/button';
|
||||
import {DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {TableModule} from 'primeng/table';
|
||||
import {LowerCasePipe, TitleCasePipe} from '@angular/common';
|
||||
@@ -29,7 +29,8 @@ import {DialogLauncherService} from '../../../shared/services/dialog-launcher.se
|
||||
Password,
|
||||
LowerCasePipe,
|
||||
TitleCasePipe,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
ButtonDirective
|
||||
],
|
||||
templateUrl: './user-management.component.html',
|
||||
styleUrls: ['./user-management.component.scss'],
|
||||
@@ -46,6 +47,7 @@ export class UserManagementComponent implements OnInit, OnDestroy {
|
||||
currentUser: User | null = null;
|
||||
editingLibraryIds: number[] = [];
|
||||
allLibraries: Library[] = [];
|
||||
expandedRows: { [key: string]: boolean } = {};
|
||||
|
||||
isPasswordDialogVisible = false;
|
||||
selectedUser: User | null = null;
|
||||
@@ -215,4 +217,82 @@ export class UserManagementComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getBookManagementPermissionsCount(user: User): number {
|
||||
const permissions = user.permissions;
|
||||
let count = 0;
|
||||
if (permissions.canUpload) count++;
|
||||
if (permissions.canDownload) count++;
|
||||
if (permissions.canDeleteBook) count++;
|
||||
if (permissions.canEditMetadata) count++;
|
||||
if (permissions.canManageLibrary) count++;
|
||||
if (permissions.canEmailBook) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
getDeviceSyncPermissionsCount(user: User): number {
|
||||
const permissions = user.permissions;
|
||||
let count = 0;
|
||||
if (permissions.canSyncKoReader) count++;
|
||||
if (permissions.canSyncKobo) count++;
|
||||
if (permissions.canAccessOpds) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
getSystemAccessPermissionsCount(user: User): number {
|
||||
const permissions = user.permissions;
|
||||
let count = 0;
|
||||
if (permissions.canAccessBookdrop) count++;
|
||||
if (permissions.canAccessLibraryStats) count++;
|
||||
if (permissions.canAccessUserStats) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
getSystemConfigPermissionsCount(user: User): number {
|
||||
const permissions = user.permissions;
|
||||
let count = 0;
|
||||
if (permissions.canAccessTaskManager) count++;
|
||||
if (permissions.canManageGlobalPreferences) count++;
|
||||
if (permissions.canManageMetadataConfig) count++;
|
||||
if (permissions.canManageIcons) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
toggleRowExpansion(user: User) {
|
||||
if (this.expandedRows[user.id]) {
|
||||
delete this.expandedRows[user.id];
|
||||
} else {
|
||||
this.expandedRows[user.id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
isRowExpanded(user: User): boolean {
|
||||
return this.expandedRows[user.id];
|
||||
}
|
||||
|
||||
onAdminCheckboxChange(user: any) {
|
||||
if (user.permissions.admin) {
|
||||
user.permissions.canUpload = true;
|
||||
user.permissions.canDownload = true;
|
||||
user.permissions.canDeleteBook = true;
|
||||
user.permissions.canEditMetadata = true;
|
||||
user.permissions.canManageLibrary = true;
|
||||
user.permissions.canEmailBook = true;
|
||||
user.permissions.canSyncKoReader = true;
|
||||
user.permissions.canSyncKobo = true;
|
||||
user.permissions.canAccessOpds = true;
|
||||
user.permissions.canAccessBookdrop = true;
|
||||
user.permissions.canAccessLibraryStats = true;
|
||||
user.permissions.canAccessUserStats = true;
|
||||
user.permissions.canManageMetadataConfig = true;
|
||||
user.permissions.canManageGlobalPreferences = true;
|
||||
user.permissions.canAccessTaskManager = true;
|
||||
user.permissions.canManageEmailConfig = true;
|
||||
user.permissions.canManageIcons = true;
|
||||
}
|
||||
}
|
||||
|
||||
isPermissionDisabled(user: any): boolean {
|
||||
return !user.isEditing || user.permissions.admin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +158,19 @@ export interface User {
|
||||
canEmailBook: boolean;
|
||||
canDeleteBook: boolean;
|
||||
canEditMetadata: boolean;
|
||||
canManipulateLibrary: boolean;
|
||||
canManageLibrary: boolean;
|
||||
canManageMetadataConfig: boolean;
|
||||
canSyncKoReader: boolean;
|
||||
canSyncKobo: boolean;
|
||||
canAccessOpds: boolean;
|
||||
canAccessBookdrop: boolean;
|
||||
canAccessLibraryStats: boolean;
|
||||
canAccessUserStats: boolean;
|
||||
canAccessTaskManager: boolean;
|
||||
canManageEmailConfig: boolean;
|
||||
canManageGlobalPreferences: boolean;
|
||||
canManageIcons: boolean;
|
||||
demoUser: boolean;
|
||||
};
|
||||
userSettings: UserSettings;
|
||||
provisioningMethod?: 'LOCAL' | 'OIDC' | 'REMOTE';
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<i class="pi pi-chart-line"></i>
|
||||
<h2>{{ userName ? userName + "'s Reading Statistics" : "Your Reading Statistics" }}</h2>
|
||||
</div>
|
||||
<p class="subtitle">Track your reading habits and progress</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -51,14 +51,6 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
@@ -100,12 +92,6 @@
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.875rem;
|
||||
padding-left: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
@@ -133,12 +119,6 @@
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.8rem;
|
||||
padding-left: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<p-tablist>
|
||||
<p-tab value="0">Prime Icons</p-tab>
|
||||
<p-tab value="1">SVG Icons</p-tab>
|
||||
@if (canManageIcons) {
|
||||
<p-tab value="2">Add SVG Icon(s)</p-tab>
|
||||
}
|
||||
</p-tablist>
|
||||
<p-tabpanels>
|
||||
<p-tabpanel value="0">
|
||||
@@ -71,21 +73,23 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (canManageIcons) {
|
||||
<div style="display: flex; align-items: center; position: fixed; right: 32px; bottom: 32px; z-index: 101;">
|
||||
<div
|
||||
class="svg-trash-area"
|
||||
[class.trash-hover]="isTrashHover"
|
||||
(dragover)="onTrashDragOver($event)"
|
||||
(dragleave)="onTrashDragLeave($event)"
|
||||
(drop)="onTrashDrop($event)"
|
||||
>
|
||||
(drop)="onTrashDrop($event)">
|
||||
<i class="pi pi-trash"></i>
|
||||
<span>Drag here to delete icon</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
@if (canManageIcons) {
|
||||
<p-tabpanel value="2">
|
||||
<div class="svg-paste-container">
|
||||
<div class="svg-input-section">
|
||||
@@ -179,5 +183,6 @@
|
||||
}
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
}
|
||||
</p-tabpanels>
|
||||
</p-tabs>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {MessageService} from 'primeng/api';
|
||||
import {IconCategoriesHelper} from '../../helpers/icon-categories.helper';
|
||||
import {Button} from 'primeng/button';
|
||||
import {TabsModule} from 'primeng/tabs';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
|
||||
interface SvgEntry {
|
||||
name: string;
|
||||
@@ -65,6 +66,7 @@ export class IconPickerComponent implements OnInit {
|
||||
sanitizer = inject(DomSanitizer);
|
||||
urlHelper = inject(UrlHelperService);
|
||||
messageService = inject(MessageService);
|
||||
userService = inject(UserService);
|
||||
|
||||
searchText: string = '';
|
||||
selectedIcon: string | null = null;
|
||||
@@ -399,4 +401,9 @@ export class IconPickerComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get canManageIcons(): boolean {
|
||||
const user = this.userService.getCurrentUser();
|
||||
return user?.permissions.canManageIcons || user?.permissions.admin || false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export class AppMenuitemComponent implements OnInit, OnDestroy {
|
||||
) {
|
||||
this.userService.userState$.subscribe(userState => {
|
||||
if (userState?.user) {
|
||||
this.canManipulateLibrary = userState.user.permissions.canManipulateLibrary;
|
||||
this.canManipulateLibrary = userState.user.permissions.canManageLibrary;
|
||||
this.admin = userState.user.permissions.admin;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,47 +30,51 @@
|
||||
<ul class="topbar-items hidden md:flex items-center gap-3 ml-auto pl-4">
|
||||
<div class="flex gap-4">
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (userState.user?.permissions?.canAccessBookdrop || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
|
||||
<a class="topbar-item" (click)="navigateToBookdrop()" pTooltip="Bookdrop" tooltipPosition="bottom">
|
||||
<i class="pi pi-inbox text-surface-100"></i>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
@if (userState.user?.permissions?.canManageLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
|
||||
<a class="topbar-item" (click)="openLibraryCreatorDialog()" pTooltip="Create New Library" tooltipPosition="bottom">
|
||||
<i class="pi pi-plus-circle text-surface-100"></i>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
}
|
||||
@if (userState.user?.permissions?.canUpload || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<a class="topbar-item" (click)="openFileUploadDialog()" pTooltip="Upload Book" tooltipPosition="bottom">
|
||||
<i class="pi pi-upload text-surface-100"></i>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@if (hasStatsAccess) {
|
||||
<li>
|
||||
<button
|
||||
class="topbar-item"
|
||||
(click)="statsMenu.toggle($event)"
|
||||
pTooltip="Stats"
|
||||
(click)="shouldShowStatsMenu ? statsMenu.toggle($event) : handleStatsButtonClick($event)"
|
||||
[pTooltip]="statsTooltip"
|
||||
tooltipPosition="bottom">
|
||||
<i class="pi pi-chart-bar text-surface-100"></i>
|
||||
</button>
|
||||
@if (shouldShowStatsMenu) {
|
||||
<p-menu #statsMenu [model]="statsMenuItems" [popup]="true" appendTo="body" />
|
||||
}
|
||||
</li>
|
||||
}
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (userState.user?.permissions?.canManageLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
|
||||
<a class="topbar-item" (click)="navigateToMetadataManager()" pTooltip="Metadata Manager" tooltipPosition="bottom">
|
||||
<i class="pi pi-sparkles text-surface-100"></i>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
<li>
|
||||
<a class="topbar-item" (click)="navigateToSettings()" pTooltip="Settings" tooltipPosition="bottom">
|
||||
<i class="pi pi-cog text-surface-100"></i>
|
||||
@@ -140,11 +144,18 @@
|
||||
<i class="pi pi-info-circle text-surface-100"></i>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Show Profile only to demo users -->
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (!userState.user?.permissions?.demoUser) {
|
||||
<li>
|
||||
<button class="topbar-item" (click)="openUserProfileDialog()" pTooltip="Profile" tooltipPosition="bottom">
|
||||
<i class="pi pi-user text-surface-100"></i>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
<li>
|
||||
<button class="topbar-item" (click)="logout()" pTooltip="Logout" tooltipPosition="left">
|
||||
<i class="pi pi-sign-out text-surface-100"></i>
|
||||
@@ -162,16 +173,7 @@
|
||||
<p-popover #mobileMenu>
|
||||
<ul class="flex flex-col gap-1 w-48">
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openLibraryCreatorDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-plus-circle text-surface-100"></i>
|
||||
Create Library
|
||||
</button>
|
||||
</li>
|
||||
@if (userState.user?.permissions?.canAccessBookdrop || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
@@ -182,6 +184,17 @@
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
@if (userState.user?.permissions?.canManageLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openLibraryCreatorDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-plus-circle text-surface-100"></i>
|
||||
Create Library
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
@if (userState.user?.permissions?.canUpload || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
@@ -194,16 +207,33 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@if (hasStatsAccess) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="statsMenu.toggle($event)"
|
||||
(click)="shouldShowStatsMenu ? statsMenuMobile.toggle($event) : handleStatsButtonClick($event)"
|
||||
>
|
||||
<i class="pi pi-chart-bar text-surface-100"></i>
|
||||
Charts
|
||||
</button>
|
||||
@if (shouldShowStatsMenu) {
|
||||
<p-menu #statsMenuMobile [model]="statsMenuItems" [popup]="true" />
|
||||
}
|
||||
</li>
|
||||
}
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (userState.user?.permissions?.canManageLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="navigateToMetadataManager(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-sparkles text-surface-100"></i>
|
||||
Metadata Manager
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
@@ -232,6 +262,9 @@
|
||||
Support BookLore
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (!userState.user?.permissions?.demoUser) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
@@ -241,6 +274,9 @@
|
||||
Profile
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
|
||||
@@ -57,6 +57,8 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
@ViewChild('menubutton') menuButton!: ElementRef;
|
||||
@ViewChild('topbarmenubutton') topbarMenuButton!: ElementRef;
|
||||
@ViewChild('topbarmenu') menu!: ElementRef;
|
||||
@ViewChild('statsMenu') statsMenu: any;
|
||||
@ViewChild('statsMenuMobile') statsMenuMobile: any;
|
||||
|
||||
isMenuVisible = true;
|
||||
progressHighlight = false;
|
||||
@@ -83,7 +85,6 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
private bookdropFileService: BookdropFileService,
|
||||
private dialogLauncher: DialogLauncherService
|
||||
) {
|
||||
this.initializeStatsMenu();
|
||||
this.subscribeToMetadataProgress();
|
||||
this.subscribeToNotifications();
|
||||
|
||||
@@ -104,6 +105,12 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
this.updateCompletedTaskCount();
|
||||
this.updateTaskVisibilityWithBookdrop();
|
||||
});
|
||||
|
||||
this.userService.userState$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.initializeStatsMenu();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -158,6 +165,16 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
handleStatsButtonClick(event: Event) {
|
||||
if (this.statsMenuItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.statsMenuItems.length === 1) {
|
||||
this.statsMenuItems[0].command?.({originalEvent: event, item: this.statsMenuItems[0]});
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToMetadataProgress() {
|
||||
this.metadataProgressService.progressUpdates$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -200,18 +217,44 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
private initializeStatsMenu() {
|
||||
this.statsMenuItems = [
|
||||
{
|
||||
const userState = this.userService.userStateSubject.value;
|
||||
const user = userState.user;
|
||||
|
||||
this.statsMenuItems = [];
|
||||
|
||||
if (user?.permissions?.canAccessLibraryStats || user?.permissions?.admin) {
|
||||
this.statsMenuItems.push({
|
||||
label: 'Library Stats',
|
||||
icon: 'pi pi-chart-line',
|
||||
command: () => this.navigateToStats()
|
||||
},
|
||||
{
|
||||
});
|
||||
}
|
||||
|
||||
if (user?.permissions?.canAccessUserStats || user?.permissions?.admin) {
|
||||
this.statsMenuItems.push({
|
||||
label: 'Reading Stats',
|
||||
icon: 'pi pi-users',
|
||||
command: () => this.navigateToUserStats()
|
||||
});
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
get hasStatsAccess(): boolean {
|
||||
return this.statsMenuItems.length > 0;
|
||||
}
|
||||
|
||||
get shouldShowStatsMenu(): boolean {
|
||||
return this.statsMenuItems.length > 1;
|
||||
}
|
||||
|
||||
get statsTooltip(): string {
|
||||
if (this.statsMenuItems.length === 0) {
|
||||
return 'Stats';
|
||||
}
|
||||
if (this.statsMenuItems.length === 1) {
|
||||
return this.statsMenuItems[0].label || 'Stats';
|
||||
}
|
||||
return 'Stats';
|
||||
}
|
||||
|
||||
get iconClass(): string {
|
||||
|
||||
@@ -184,7 +184,7 @@ export class DialogLauncherService {
|
||||
openIconPickerDialog(): DynamicDialogRef | null {
|
||||
return this.openDialog(IconPickerComponent, {
|
||||
header: 'Choose an Icon',
|
||||
styleClass: 'dialog-maximal',
|
||||
styleClass: 'dialog-medium',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user