Lots of permission related stuffs

This commit is contained in:
aditya.chandel
2025-02-17 09:25:04 -07:00
parent 72314820de
commit 13066b8c0a
17 changed files with 158 additions and 21 deletions

View File

@@ -10,6 +10,7 @@ import org.mapstruct.Mapping;
public interface BookLoreUserMapper {
@Mapping(source = "permissions", target = "permissions")
@Mapping(source = "libraries", target = "assignedLibraries")
BookLoreUser toDto(BookLoreUserEntity entity);
default BookLoreUser.UserPermissions mapPermissions(UserPermissionsEntity permissions) {
@@ -22,7 +23,6 @@ public interface BookLoreUserMapper {
dto.setCanDownload(permissions.isPermissionDownload());
dto.setCanManipulateLibrary(permissions.isPermissionManipulateLibrary());
dto.setCanEditMetadata(permissions.isPermissionEditMetadata());
dto.setCanEditMetadata(permissions.isPermissionEditMetadata());
return dto;
}
}
}

View File

@@ -2,12 +2,15 @@ package com.adityachandel.booklore.model.dto;
import lombok.Data;
import java.util.List;
@Data
public class BookLoreUser {
private Long id;
private String username;
private String name;
private String email;
private List<Library> assignedLibraries;
private UserPermissions permissions;
@Data

View File

@@ -2,6 +2,8 @@ package com.adityachandel.booklore.model.dto;
import lombok.Data;
import java.util.Set;
@Data
public class UserCreateRequest {
private String username;
@@ -13,4 +15,6 @@ public class UserCreateRequest {
private boolean permissionDownload;
private boolean permissionEditMetadata;
private boolean permissionManipulateLibrary;
private Set<Long> selectedLibraries;
}

View File

@@ -2,11 +2,14 @@ package com.adityachandel.booklore.model.dto.request;
import lombok.Data;
import java.util.List;
@Data
public class UserUpdateRequest {
private String name;
private String email;
private Permissions permissions;
private List<Long> assignedLibraries;
@Data
public static class Permissions {

View File

@@ -5,6 +5,7 @@ import lombok.*;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Getter
@@ -44,6 +45,14 @@ public class BookLoreUserEntity {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private Set<ShelfEntity> shelves = new HashSet<>();
@ManyToMany
@JoinTable(
name = "user_library_mapping",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "library_id")
)
private List<LibraryEntity> libraries;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();

View File

@@ -31,6 +31,9 @@ public class LibraryEntity {
@OneToMany(mappedBy = "library", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<LibraryPathEntity> libraryPaths;
@ManyToMany(mappedBy = "libraries")
private List<BookLoreUserEntity> users;
private boolean watch;
private String icon;

View File

@@ -1,6 +1,7 @@
package com.adityachandel.booklore.repository;
import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.LibraryEntity;
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -14,6 +15,7 @@ import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Repository
public interface BookRepository extends JpaRepository<BookEntity, Long>, JpaSpecificationExecutor<BookEntity> {
@@ -29,14 +31,16 @@ public interface BookRepository extends JpaRepository<BookEntity, Long>, JpaSpec
Optional<BookEntity> findBookByFileNameAndLibraryId(String fileName, long libraryId);
@Query("SELECT b FROM BookEntity b JOIN b.metadata bm WHERE LOWER(bm.title) LIKE LOWER(CONCAT('%', :title, '%'))")
List<BookEntity> findByTitleContainingIgnoreCase(@Param("title") String title);
@Query("SELECT b FROM BookEntity b JOIN b.shelves s WHERE s.id = :shelfId")
List<BookEntity> findByShelfId(@Param("shelfId") Long shelfId);
@Modifying
@Query("DELETE FROM BookEntity b WHERE b.id IN (:ids)")
void deleteByIdIn(Collection<Long> ids);
@Query("SELECT b FROM BookEntity b WHERE b.library IN (SELECT l FROM LibraryEntity l WHERE l IN :userLibraries)")
List<BookEntity> findBooksByUserLibraries(@Param("userLibraries") List<LibraryEntity> userLibraries);
List<BookEntity> findByLibraryIdIn(Set<Long> userLibraryIds);
}

View File

@@ -3,9 +3,14 @@ package com.adityachandel.booklore.repository;
import com.adityachandel.booklore.model.entity.LibraryEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface LibraryRepository extends JpaRepository<LibraryEntity, Long>, JpaSpecificationExecutor<LibraryEntity> {
List<LibraryEntity> findByIdIn(List<Long> ids);
}

View File

@@ -97,8 +97,29 @@ public class BooksService {
}
public List<Book> getBooks(boolean withDescription) {
return bookRepository.findAll().stream()
.map(bookEntity -> bookMapper.toBookWithDescription(bookEntity, withDescription))
BookLoreUser user = authenticationService.getAuthenticatedUser();
BookLoreUserEntity userEntity = userRepository.findById(user.getId()).orElseThrow(() -> new UsernameNotFoundException("User not found"));
List<BookEntity> books;
if (userEntity.getPermissions().isPermissionAdmin()) {
books = bookRepository.findAll();
} else {
Set<Long> userLibraryIds = userEntity.getLibraries().stream()
.map(LibraryEntity::getId)
.collect(Collectors.toSet());
books = bookRepository.findByLibraryIdIn(userLibraryIds);
}
return books.stream()
.map(bookEntity -> {
UserBookProgressEntity userProgress = userBookProgressRepository.findByUserIdAndBookId(user.getId(), bookEntity.getId())
.orElse(new UserBookProgressEntity());
Book book = bookMapper.toBookWithDescription(bookEntity, withDescription);
book.setLastReadTime(userProgress.getLastReadTime());
book.setPdfProgress(userProgress.getPdfProgress());
book.setEpubProgress(userProgress.getEpubProgress());
return book;
})
.collect(Collectors.toList());
}

View File

@@ -4,16 +4,19 @@ import com.adityachandel.booklore.exception.ApiError;
import com.adityachandel.booklore.mapper.BookMapper;
import com.adityachandel.booklore.mapper.LibraryMapper;
import com.adityachandel.booklore.model.dto.Book;
import com.adityachandel.booklore.model.dto.BookLoreUser;
import com.adityachandel.booklore.model.dto.Library;
import com.adityachandel.booklore.model.dto.LibraryPath;
import com.adityachandel.booklore.model.dto.request.CreateLibraryRequest;
import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.BookLoreUserEntity;
import com.adityachandel.booklore.model.entity.LibraryEntity;
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
import com.adityachandel.booklore.model.websocket.Topic;
import com.adityachandel.booklore.repository.BookRepository;
import com.adityachandel.booklore.repository.LibraryPathRepository;
import com.adityachandel.booklore.repository.LibraryRepository;
import com.adityachandel.booklore.repository.UserRepository;
import com.adityachandel.booklore.service.fileprocessor.FileProcessingUtils;
import com.adityachandel.booklore.service.monitoring.MonitoringService;
import jakarta.annotation.PostConstruct;
@@ -21,6 +24,7 @@ import jakarta.transaction.Transactional;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.io.IOException;
@@ -45,6 +49,8 @@ public class LibraryService {
private final NotificationService notificationService;
private final FileProcessingUtils fileProcessingUtils;
private final MonitoringService monitoringService;
private final AuthenticationService authenticationService;
private final UserRepository userRepository;
@Transactional
@PostConstruct
@@ -188,7 +194,15 @@ public class LibraryService {
}
public List<Library> getLibraries() {
List<LibraryEntity> libraries = libraryRepository.findAll();
BookLoreUser user = authenticationService.getAuthenticatedUser();
BookLoreUserEntity userEntity = userRepository.findById(user.getId()).orElseThrow(() -> new UsernameNotFoundException("User not found"));
List<LibraryEntity> libraries;
if (userEntity.getPermissions().isPermissionAdmin()) {
libraries = libraryRepository.findAll();
} else {
List<Long> libraryIds = userEntity.getLibraries().stream().map(LibraryEntity::getId).toList();
libraries = libraryRepository.findByIdIn(libraryIds);
}
return libraries.stream().map(libraryMapper::toLibrary).toList();
}

View File

@@ -8,8 +8,11 @@ import com.adityachandel.booklore.model.dto.UserCreateRequest;
import com.adityachandel.booklore.model.dto.request.UserLoginRequest;
import com.adityachandel.booklore.model.dto.request.UserUpdateRequest;
import com.adityachandel.booklore.model.entity.BookLoreUserEntity;
import com.adityachandel.booklore.model.entity.LibraryEntity;
import com.adityachandel.booklore.model.entity.UserPermissionsEntity;
import com.adityachandel.booklore.repository.LibraryRepository;
import com.adityachandel.booklore.repository.UserRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -18,10 +21,7 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
@Service
@@ -31,8 +31,10 @@ public class UserService {
private final UserRepository userRepository;
private final BookLoreUserMapper bookLoreUserMapper;
private final PasswordEncoder passwordEncoder;
private final LibraryRepository libraryRepository;
private final JwtUtils jwtUtils;
@Transactional
public ResponseEntity<Map<String, String>> registerUser(UserCreateRequest request) {
Optional<BookLoreUserEntity> existingUser = userRepository.findByUsername(request.getUsername());
if (existingUser.isPresent()) {
@@ -50,10 +52,14 @@ public class UserService {
permissions.setPermissionUpload(request.isPermissionUpload());
permissions.setPermissionDownload(request.isPermissionDownload());
permissions.setPermissionEditMetadata(request.isPermissionEditMetadata());
permissions.setPermissionEditMetadata(request.isPermissionManipulateLibrary());
permissions.setPermissionManipulateLibrary(request.isPermissionManipulateLibrary());
bookLoreUserEntity.setPermissions(permissions);
if (request.getSelectedLibraries() != null && !request.getSelectedLibraries().isEmpty()) {
List<LibraryEntity> libraries = libraryRepository.findAllById(request.getSelectedLibraries());
bookLoreUserEntity.setLibraries(new ArrayList<>(libraries));
}
userRepository.save(bookLoreUserEntity);
String token = jwtUtils.generateToken(bookLoreUserEntity);
@@ -84,11 +90,19 @@ public class UserService {
public BookLoreUser updateUser(Long id, UserUpdateRequest updateRequest) {
BookLoreUserEntity user = userRepository.findById(id).orElseThrow(() -> ApiError.USER_NOT_FOUND.createException(id));
user.setName(updateRequest.getName());
user.setEmail(updateRequest.getEmail());
user.getPermissions().setPermissionUpload(updateRequest.getPermissions().isCanUpload());
user.getPermissions().setPermissionDownload(updateRequest.getPermissions().isCanDownload());
user.getPermissions().setPermissionEditMetadata(updateRequest.getPermissions().isCanEditMetadata());
List<Long> libraryIds = updateRequest.getAssignedLibraries();
if (libraryIds != null) {
List<LibraryEntity> updatedLibraries = libraryRepository.findAllById(libraryIds);
user.setLibraries(updatedLibraries);
}
userRepository.save(user);
return bookLoreUserMapper.toDto(user);
}

View File

@@ -206,4 +206,14 @@ CREATE TABLE IF NOT EXISTS user_book_progress
);
CREATE INDEX IF NOT EXISTS idx_user_book_progress_user ON user_book_progress (user_id);
CREATE INDEX IF NOT EXISTS idx_user_book_progress_book ON user_book_progress (book_id);
CREATE INDEX IF NOT EXISTS idx_user_book_progress_book ON user_book_progress (book_id);
CREATE TABLE IF NOT EXISTS user_library_mapping
(
user_id BIGINT NOT NULL,
library_id BIGINT NOT NULL,
PRIMARY KEY (user_id, library_id),
CONSTRAINT fk_user_library_mapping_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
CONSTRAINT fk_user_library_mapping_library FOREIGN KEY (library_id) REFERENCES library (id) ON DELETE CASCADE
);

View File

@@ -7,6 +7,8 @@ import {BookService} from './book.service';
import {SortOption} from '../model/sort.model';
import {LibraryState} from '../model/state/library-state.model';
import {API_CONFIG} from '../../config/api-config';
import {FetchMetadataRequest} from '../../metadata/model/request/fetch-metadata-request.model';
import {BookMetadata} from '../model/book.model';
@Injectable({
providedIn: 'root',
@@ -138,4 +140,8 @@ export class LibraryService {
getLibrariesFromState(): Library[] {
return this.libraryStateSubject.value.libraries || [];
}
getAllLibrariesFromAPI(): Observable<Library[]> {
return this.http.get <Library[]>(`${this.url}`);
}
}

View File

@@ -7,6 +7,7 @@
<th>Username</th>
<th>Full Name</th>
<th>Email</th>
<th>Assigned Libraries</th>
<th style="width: 100px;">Can Upload</th>
<th style="width: 100px;">Can Download</th>
<th style="width: 100px;">Can Edit Metadata</th>
@@ -26,6 +27,20 @@
<input *ngIf="user.isEditing" type="email" [(ngModel)]="user.email" class="p-inputtext w-full"/>
<span *ngIf="!user.isEditing">{{ user.email }}</span>
</td>
<td>
<p-multiSelect
*ngIf="user.isEditing"
[options]="allLibraries"
optionLabel="name"
optionValue="id"
[(ngModel)]="editingLibraryIds"
placeholder="Select Libraries"
appendTo="body">
</p-multiSelect>
<span *ngIf="!user.isEditing">
{{ user.libraryNames }}
</span>
</td>
<td class="text-center">
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canUpload" *ngIf="user.isEditing"></p-checkbox>
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canUpload" disabled *ngIf="!user.isEditing"></p-checkbox>

View File

@@ -8,23 +8,37 @@ import {NgIf, NgStyle} from '@angular/common';
import {User, UserService} from '../../../../user.service';
import {MessageService} from 'primeng/api';
import {Checkbox} from 'primeng/checkbox';
import {MultiSelect} from 'primeng/multiselect';
import {Library} from '../../../../book/model/library.model';
import {LibraryService} from '../../../../book/service/library.service';
@Component({
selector: 'app-admin',
imports: [FormsModule, Button, TableModule, NgIf, Checkbox, NgStyle],
imports: [FormsModule, Button, TableModule, NgIf, Checkbox, NgStyle, MultiSelect],
templateUrl: './admin.component.html',
styleUrl: './admin.component.scss',
styleUrls: ['./admin.component.scss'],
})
export class AdminComponent implements OnInit {
ref: DynamicDialogRef | undefined;
private dialogService = inject(DialogService);
private userService = inject(UserService);
private libraryService = inject(LibraryService);
private messageService = inject(MessageService);
users: any[] = [];
users: User[] = [];
editingLibraryIds: number[] = [];
allLibraries: Library[] = [];
ngOnInit() {
this.loadUsers();
this.libraryService.getAllLibrariesFromAPI().subscribe({
next: (libraries) => {
this.allLibraries = libraries;
},
error: () => {
this.messageService.add({severity: 'error', summary: 'Error', detail: 'Failed to load libraries'});
}
});
}
loadUsers() {
@@ -33,6 +47,8 @@ export class AdminComponent implements OnInit {
this.users = data.map(user => ({
...user,
isEditing: false,
selectedLibraryIds: user.assignedLibraries?.map(lib => lib.id) || [],
libraryNames: user.assignedLibraries?.map(lib => lib.name).join(', ') || ''
}));
},
error: () => {
@@ -57,19 +73,24 @@ export class AdminComponent implements OnInit {
toggleEdit(user: any) {
user.isEditing = !user.isEditing;
if (!user.isEditing) {
this.loadUsers();
if (user.isEditing) {
this.editingLibraryIds = [...user.selectedLibraryIds];
} else {
user.libraryNames = user.assignedLibraries?.map((lib: Library) => lib.name).join(', ') || '';
}
}
saveUser(user: any) {
user.selectedLibraryIds = [...this.editingLibraryIds];
this.userService.updateUser(user.id, {
name: user.name,
email: user.email,
permissions: user.permissions,
assignedLibraries: user.selectedLibraryIds
}).subscribe({
next: () => {
user.isEditing = false;
this.loadUsers();
this.messageService.add({severity: 'success', summary: 'Success', detail: 'User updated successfully'});
},
error: () => {

View File

@@ -61,7 +61,10 @@ export class CreateUserDialogComponent implements OnInit {
return;
}
const userData = this.userForm.value;
const userData = {
...this.userForm.value,
selectedLibraries: this.userForm.value.selectedLibraries.map((lib: Library) => lib.id)
};
this.userService.createUser(userData).subscribe({
next: response => {

View File

@@ -4,12 +4,14 @@ import {BehaviorSubject, Observable} from 'rxjs';
import {API_CONFIG} from './config/api-config';
import {jwtDecode} from 'jwt-decode';
import {RxStompService} from './shared/websocket/rx-stomp.service';
import {Library} from './book/model/library.model';
export interface User {
id: number;
username: string;
name: string;
email: string;
assignedLibraries: Library[];
permissions: {
admin: boolean;
canUpload: boolean;