Restricted Profiles (#1581)
* Added ReadingList age rating from all series and started on some unit tests for the new flows. * Wrote more unit tests for Reading Lists * Added ability to restrict user accounts to a given age rating via admin edit user modal and invite user. This commit contains all basic code, but no query modifications. * When updating a reading list's title via UI, explicitly check if there is an existing RL with the same title. * Refactored Reading List calculation to work properly in the flows it's invoked from. * Cleaned up an unused method * Promoted Collections no longer show tags where a Series exists within them that is above the user's age rating. * Collection search now respects age restrictions * Series Detail page now checks if the user has explicit access (as a user might bypass with direct url access) * Hooked up age restriction for dashboard activity streams. * Refactored some methods from Series Controller and Library Controller to a new Search Controller to keep things organized * Updated Search to respect age restrictions * Refactored all the Age Restriction queries to extensions * Related Series no longer show up if they are out of the age restriction * Fixed a bad mapping for the update age restriction api * Fixed a UI state change after updating age restriction * Fixed unit test * Added a migration for reading lists * Code cleanup
This commit is contained in:
parent
0ad1638ec0
commit
442af965c6
63 changed files with 4638 additions and 262 deletions
|
@ -12,7 +12,6 @@ using API.DTOs.Account;
|
|||
using API.DTOs.Email;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums.UserPreferences;
|
||||
using API.Errors;
|
||||
using API.Extensions;
|
||||
using API.Services;
|
||||
|
@ -358,6 +357,34 @@ public class AccountController : BaseApiController
|
|||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("update/age-restriction")]
|
||||
public async Task<ActionResult> UpdateAgeRestriction(UpdateAgeRestrictionDto dto)
|
||||
{
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
if (user == null) return Unauthorized("You do not have permission");
|
||||
if (dto == null) return BadRequest("Invalid payload");
|
||||
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
user.AgeRestriction = isAdmin ? AgeRating.NotApplicable : dto.AgeRestriction;
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return Ok();
|
||||
try
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an error updating the age restriction");
|
||||
return BadRequest("There was an error updating the age restriction");
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the user account. This can only affect Username, Email (will require confirming), Roles, and Library access.
|
||||
/// </summary>
|
||||
|
@ -428,6 +455,9 @@ public class AccountController : BaseApiController
|
|||
lib.AppUsers.Add(user);
|
||||
}
|
||||
|
||||
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction;
|
||||
_unitOfWork.UserRepository.Update(user);
|
||||
|
||||
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
||||
{
|
||||
await _eventHub.SendMessageToAsync(MessageFactory.UserUpdate, MessageFactory.UserUpdateEvent(user.Id, user.UserName), user.Id);
|
||||
|
@ -540,6 +570,8 @@ public class AccountController : BaseApiController
|
|||
lib.AppUsers.Add(user);
|
||||
}
|
||||
|
||||
user.AgeRestriction = hasAdminRole ? AgeRating.NotApplicable : dto.AgeRestriction;
|
||||
|
||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
|
|
|
@ -41,7 +41,8 @@ public class CollectionController : BaseApiController
|
|||
{
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||
}
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
||||
|
||||
return await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(user.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -56,9 +57,10 @@ public class CollectionController : BaseApiController
|
|||
{
|
||||
queryString ??= "";
|
||||
queryString = queryString.Replace(@"%", string.Empty);
|
||||
if (queryString.Length == 0) return await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync();
|
||||
if (queryString.Length == 0) return await GetAllTags();
|
||||
|
||||
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString);
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
return await _unitOfWork.CollectionTagRepository.SearchTagDtosAsync(queryString, user.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -319,23 +319,6 @@ public class LibraryController : BaseApiController
|
|||
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
|
||||
{
|
||||
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
|
||||
[HttpGet("type")]
|
||||
public async Task<ActionResult<LibraryType>> GetLibraryType(int libraryId)
|
||||
|
|
|
@ -76,7 +76,9 @@ public class MetadataController : BaseApiController
|
|||
/// Fetches all age ratings from the instance
|
||||
/// </summary>
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
|
||||
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(Duration = 60 * 5, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"libraryIds"})]
|
||||
[HttpGet("age-ratings")]
|
||||
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
|
||||
{
|
||||
|
@ -90,14 +92,16 @@ public class MetadataController : BaseApiController
|
|||
{
|
||||
Title = t.ToDescription(),
|
||||
Value = t
|
||||
}));
|
||||
}).Where(r => r.Value > AgeRating.NotApplicable));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches all publication status' from the instance
|
||||
/// </summary>
|
||||
/// <param name="libraryIds">String separated libraryIds or null for all publication status</param>
|
||||
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
|
||||
/// <returns></returns>
|
||||
[ResponseCache(Duration = 60 * 5, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"libraryIds"})]
|
||||
[HttpGet("publication-status")]
|
||||
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
|
||||
{
|
||||
|
|
|
@ -196,8 +196,8 @@ public class OpdsController : BaseApiController
|
|||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
IList<CollectionTagDto> tags = isAdmin ? (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync()).ToList()
|
||||
: (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync()).ToList();
|
||||
IEnumerable<CollectionTagDto> tags = isAdmin ? (await _unitOfWork.CollectionTagRepository.GetAllTagDtosAsync())
|
||||
: (await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId));
|
||||
|
||||
|
||||
var feed = CreateFeed("All Collections", $"{apiKey}/collections", apiKey);
|
||||
|
@ -239,7 +239,7 @@ public class OpdsController : BaseApiController
|
|||
}
|
||||
else
|
||||
{
|
||||
tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync();
|
||||
tags = await _unitOfWork.CollectionTagRepository.GetAllPromotedTagDtosAsync(userId);
|
||||
}
|
||||
|
||||
var tag = tags.SingleOrDefault(t => t.Id == collectionId);
|
||||
|
|
|
@ -45,11 +45,11 @@ public class ReadingListController : BaseApiController
|
|||
/// <summary>
|
||||
/// Returns reading lists (paginated) for a given user.
|
||||
/// </summary>
|
||||
/// <param name="includePromoted">Defaults to true</param>
|
||||
/// <param name="includePromoted">Include Promoted Reading Lists along with user's Reading Lists. Defaults to true</param>
|
||||
/// <param name="userParams">Pagination parameters</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("lists")]
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, [FromQuery] bool includePromoted = true)
|
||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetListsForUser([FromQuery] UserParams userParams, bool includePromoted = true)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var items = await _unitOfWork.ReadingListRepository.GetReadingListDtosForUserAsync(userId, includePromoted,
|
||||
|
@ -217,9 +217,15 @@ public class ReadingListController : BaseApiController
|
|||
return BadRequest("You do not have permissions on this reading list or the list doesn't exist");
|
||||
}
|
||||
|
||||
dto.Title = dto.Title.Trim();
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
{
|
||||
readingList.Title = dto.Title; // Should I check if this is unique?
|
||||
var hasExisting = user.ReadingLists.Any(l => l.Title.Equals(dto.Title));
|
||||
if (hasExisting)
|
||||
{
|
||||
return BadRequest("A list of this name already exists");
|
||||
}
|
||||
readingList.Title = dto.Title;
|
||||
readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
|
|
67
API/Controllers/SearchController.cs
Normal file
67
API/Controllers/SearchController.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Search;
|
||||
using API.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for the Search interface from the UI
|
||||
/// </summary>
|
||||
public class SearchController : BaseApiController
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
public SearchController(IUnitOfWork unitOfWork)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
|
||||
/// then null is returned
|
||||
/// </summary>
|
||||
/// <param name="mangaFileId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-for-mangafile")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
|
||||
/// then null is returned
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-for-chapter")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
|
||||
}
|
||||
|
||||
[HttpGet("search")]
|
||||
public async Task<ActionResult<SearchResultGroupDto>> Search(string queryString)
|
||||
{
|
||||
queryString = Uri.UnescapeDataString(queryString).Trim().Replace(@"%", string.Empty).Replace(":", string.Empty);
|
||||
|
||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||
// Get libraries user has access to
|
||||
var libraries = (await _unitOfWork.LibraryRepository.GetLibrariesForUserIdAsync(user.Id)).ToList();
|
||||
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
if (!libraries.Any()) return BadRequest("User does not have access to any libraries");
|
||||
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
|
||||
|
||||
var series = await _unitOfWork.SeriesRepository.SearchSeries(user.Id, isAdmin, libraries.Select(l => l.Id).ToArray(), queryString);
|
||||
|
||||
return Ok(series);
|
||||
}
|
||||
}
|
|
@ -363,10 +363,13 @@ public class SeriesController : BaseApiController
|
|||
/// </summary>
|
||||
/// <param name="ageRating"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>This is cached for an hour</remarks>
|
||||
[ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"ageRating"})]
|
||||
[HttpGet("age-rating")]
|
||||
public ActionResult<string> GetAgeRating(int ageRating)
|
||||
{
|
||||
var val = (AgeRating) ageRating;
|
||||
if (val == AgeRating.NotApplicable) return "No Restriction";
|
||||
|
||||
return Ok(val.ToDescription());
|
||||
}
|
||||
|
@ -385,31 +388,7 @@ public class SeriesController : BaseApiController
|
|||
return await _seriesService.GetSeriesDetail(seriesId, userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the series for the MangaFile id. If the user does not have access (shouldn't happen by the UI),
|
||||
/// then null is returned
|
||||
/// </summary>
|
||||
/// <param name="mangaFileId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-for-mangafile")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForMangaFile(int mangaFileId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForMangaFile(mangaFileId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the series for the Chapter id. If the user does not have access (shouldn't happen by the UI),
|
||||
/// then null is returned
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("series-for-chapter")]
|
||||
public async Task<ActionResult<SeriesDto>> GetSeriesForChapter(int chapterId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
return Ok(await _unitOfWork.SeriesRepository.GetSeriesForChapter(chapterId, userId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the related series for a given series
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue