diff --git a/API/API.csproj b/API/API.csproj index d7920b926..ca3f44f85 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -6,6 +6,8 @@ true Linux true + true + true diff --git a/API/Constants/PolicyConstants.cs b/API/Constants/PolicyConstants.cs index 8e8a2bc5d..ad463411d 100644 --- a/API/Constants/PolicyConstants.cs +++ b/API/Constants/PolicyConstants.cs @@ -23,8 +23,12 @@ namespace API.Constants /// Used to give a user ability to change their own password /// public const string ChangePasswordRole = "Change Password"; + /// + /// Used to give a user ability to bookmark files on the server + /// + public const string BookmarkRole = "Bookmark"; public static readonly ImmutableArray ValidRoles = - ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole); + ImmutableArray.Create(AdminRole, PlebRole, DownloadRole, ChangePasswordRole, BookmarkRole); } } diff --git a/API/Controllers/DownloadController.cs b/API/Controllers/DownloadController.cs index 8753202f8..fc7e49807 100644 --- a/API/Controllers/DownloadController.cs +++ b/API/Controllers/DownloadController.cs @@ -31,10 +31,12 @@ namespace API.Controllers private readonly IEventHub _eventHub; private readonly ILogger _logger; private readonly IBookmarkService _bookmarkService; + private readonly IAccountService _accountService; private const string DefaultContentType = "application/octet-stream"; public DownloadController(IUnitOfWork unitOfWork, IArchiveService archiveService, IDirectoryService directoryService, - IDownloadService downloadService, IEventHub eventHub, ILogger logger, IBookmarkService bookmarkService) + IDownloadService downloadService, IEventHub eventHub, ILogger logger, IBookmarkService bookmarkService, + IAccountService accountService) { _unitOfWork = unitOfWork; _archiveService = archiveService; @@ -43,6 +45,7 @@ namespace API.Controllers _eventHub = eventHub; _logger = logger; _bookmarkService = bookmarkService; + _accountService = accountService; } /// @@ -109,7 +112,7 @@ namespace API.Controllers private async Task HasDownloadPermission() { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); - return await _downloadService.HasDownloadPermission(user); + return await _accountService.HasDownloadPermission(user); } private ActionResult GetFirstFileDownload(IEnumerable files) diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index e5165ae42..d254ff30e 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -32,6 +32,7 @@ public class OpdsController : BaseApiController private readonly ICacheService _cacheService; private readonly IReaderService _readerService; private readonly ISeriesService _seriesService; + private readonly IAccountService _accountService; private readonly XmlSerializer _xmlSerializer; @@ -65,7 +66,8 @@ public class OpdsController : BaseApiController public OpdsController(IUnitOfWork unitOfWork, IDownloadService downloadService, IDirectoryService directoryService, ICacheService cacheService, - IReaderService readerService, ISeriesService seriesService) + IReaderService readerService, ISeriesService seriesService, + IAccountService accountService) { _unitOfWork = unitOfWork; _downloadService = downloadService; @@ -73,6 +75,7 @@ public class OpdsController : BaseApiController _cacheService = cacheService; _readerService = readerService; _seriesService = seriesService; + _accountService = accountService; _xmlSerializer = new XmlSerializer(typeof(Feed)); _xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription)); @@ -619,7 +622,7 @@ public class OpdsController : BaseApiController if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) return BadRequest("OPDS is not enabled on this server"); var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey)); - if (!await _downloadService.HasDownloadPermission(user)) + if (!await _accountService.HasDownloadPermission(user)) { return BadRequest("User does not have download permissions"); } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 5569fb9f8..113f28570 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -29,17 +29,20 @@ namespace API.Controllers private readonly ILogger _logger; private readonly IReaderService _readerService; private readonly IBookmarkService _bookmarkService; + private readonly IAccountService _accountService; /// public ReaderController(ICacheService cacheService, IUnitOfWork unitOfWork, ILogger logger, - IReaderService readerService, IBookmarkService bookmarkService) + IReaderService readerService, IBookmarkService bookmarkService, + IAccountService accountService) { _cacheService = cacheService; _unitOfWork = unitOfWork; _logger = logger; _readerService = readerService; _bookmarkService = bookmarkService; + _accountService = accountService; } /// @@ -657,8 +660,13 @@ namespace API.Controllers public async Task BookmarkPage(BookmarkDto bookmarkDto) { // Don't let user save past total pages. - bookmarkDto.Page = await _readerService.CapPageToChapter(bookmarkDto.ChapterId, bookmarkDto.Page); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); + if (user == null) return new UnauthorizedResult(); + + if (!await _accountService.HasBookmarkPermission(user)) + return BadRequest("You do not have permission to bookmark"); + + bookmarkDto.Page = await _readerService.CapPageToChapter(bookmarkDto.ChapterId, bookmarkDto.Page); var chapter = await _cacheService.Ensure(bookmarkDto.ChapterId); if (chapter == null) return BadRequest("Could not find cached image. Reload and try again."); var path = _cacheService.GetCachedPagePath(chapter, bookmarkDto.Page); @@ -681,8 +689,12 @@ namespace API.Controllers public async Task UnBookmarkPage(BookmarkDto bookmarkDto) { var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername(), AppUserIncludes.Bookmarks); + if (user == null) return new UnauthorizedResult(); if (user.Bookmarks == null) return Ok(); + if (!await _accountService.HasBookmarkPermission(user)) + return BadRequest("You do not have permission to unbookmark"); + if (await _bookmarkService.RemoveBookmarkPage(user, bookmarkDto)) { BackgroundJob.Enqueue(() => _cacheService.CleanupBookmarkCache(bookmarkDto.SeriesId)); diff --git a/API/Controllers/TachiyomiController.cs b/API/Controllers/TachiyomiController.cs index f1f6a1f03..3e20220f8 100644 --- a/API/Controllers/TachiyomiController.cs +++ b/API/Controllers/TachiyomiController.cs @@ -50,7 +50,7 @@ public class TachiyomiController : BaseApiController if (prevChapterId == -1) { var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId); - var userHasProgress = series.PagesRead != 0 && series.PagesRead < series.Pages; + var userHasProgress = series.PagesRead != 0 && series.PagesRead <= series.Pages; // If the user doesn't have progress, then return null, which the extension will catch as 204 (no content) and report nothing as read if (!userHasProgress) return null; diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index 62f5386fb..80ea7a55d 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.Constants; using API.Data; using API.Entities; using API.Errors; @@ -17,6 +18,8 @@ namespace API.Services Task> ValidatePassword(AppUser user, string password); Task> ValidateUsername(string username); Task> ValidateEmail(string email); + Task HasBookmarkPermission(AppUser user); + Task HasDownloadPermission(AppUser appuser); } public class AccountService : IAccountService @@ -92,5 +95,28 @@ namespace API.Services new ApiException(400, "Email is already registered") }; } + + /// + /// Does the user have the Bookmark permission or admin rights + /// + /// + /// + public async Task HasBookmarkPermission(AppUser user) + { + var roles = await _userManager.GetRolesAsync(user); + return roles.Contains(PolicyConstants.BookmarkRole) || roles.Contains(PolicyConstants.AdminRole); + } + + /// + /// Does the user have the Download permission or admin rights + /// + /// + /// + public async Task HasDownloadPermission(AppUser user) + { + var roles = await _userManager.GetRolesAsync(user); + return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole); + } + } } diff --git a/API/Services/BookmarkService.cs b/API/Services/BookmarkService.cs index c798c47d1..b15fdd465 100644 --- a/API/Services/BookmarkService.cs +++ b/API/Services/BookmarkService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using API.Constants; using API.Data; using API.DTOs.Reader; using API.Entities; diff --git a/API/Services/DownloadService.cs b/API/Services/DownloadService.cs index c1591056a..a89a0988f 100644 --- a/API/Services/DownloadService.cs +++ b/API/Services/DownloadService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Constants; using API.Entities; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.StaticFiles; @@ -14,17 +13,12 @@ public interface IDownloadService { Tuple GetFirstFileDownload(IEnumerable files); string GetContentTypeFromFile(string filepath); - Task HasDownloadPermission(AppUser user); } public class DownloadService : IDownloadService { - private readonly UserManager _userManager; private readonly FileExtensionContentTypeProvider _fileTypeProvider = new FileExtensionContentTypeProvider(); - public DownloadService(UserManager userManager) - { - _userManager = userManager; - } + public DownloadService() { } /// /// Downloads the first file in the file enumerable for download @@ -62,9 +56,5 @@ public class DownloadService : IDownloadService return contentType; } - public async Task HasDownloadPermission(AppUser user) - { - var roles = await _userManager.GetRolesAsync(user); - return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole); - } + } diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 44ad60817..a491b80e3 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -6,6 +6,7 @@ Kavita 0.5.6.1 en + true @@ -20,4 +21,4 @@ - \ No newline at end of file + diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index 3675e818e..ab063082d 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -58,6 +58,10 @@ export class AccountService implements OnDestroy { return user && user.roles.includes('Download'); } + hasBookmarkRole(user: User) { + return user && user.roles.includes('Bookmark'); + } + getRoles() { return this.httpClient.get(this.baseUrl + 'account/roles'); } diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.html b/UI/Web/src/app/admin/edit-user/edit-user.component.html index 4ab5c3d0f..9f4039b16 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.html +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.html @@ -22,7 +22,7 @@
- +
This field is required diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/manga-reader.component.html index ebf467c3c..0e40f896f 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -18,8 +18,7 @@ Keyboard Shortcuts Modal - -