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