Merged v0.5.1 develop into main.

This commit is contained in:
Joseph Milazzo 2022-02-11 09:25:26 -06:00
commit 150479e755
256 changed files with 6898 additions and 1833 deletions

View file

@ -61,6 +61,11 @@ namespace API.Data
};
}
public static SeriesMetadata SeriesMetadata(ComicInfo info)
{
return SeriesMetadata(Array.Empty<CollectionTag>());
}
public static SeriesMetadata SeriesMetadata(ICollection<CollectionTag> collectionTags)
{
return new SeriesMetadata()

View file

@ -103,17 +103,6 @@ namespace API.Data.Metadata
info.Characters = Parser.Parser.CleanAuthor(info.Characters);
info.Translator = Parser.Parser.CleanAuthor(info.Translator);
info.CoverArtist = Parser.Parser.CleanAuthor(info.CoverArtist);
// if (!string.IsNullOrEmpty(info.Web))
// {
// // ComicVine stores the Issue number in Number field and does not use Volume.
// if (!info.Web.Contains("https://comicvine.gamespot.com/")) return;
// if (info.Volume.Equals("1"))
// {
// info.Volume = Parser.Parser.DefaultVolume;
// }
// }
}

View file

@ -9,13 +9,13 @@ using Microsoft.Extensions.Logging;
namespace API.Data;
/// <summary>
/// Responsible to migrate existing bookmarks to files
/// Responsible to migrate existing bookmarks to files. Introduced in v0.4.9.27
/// </summary>
public static class MigrateBookmarks
{
private static readonly Version VersionBookmarksChanged = new Version(0, 4, 9, 27);
/// <summary>
/// This will migrate existing bookmarks to bookmark folder based
/// This will migrate existing bookmarks to bookmark folder based.
/// If the bookmarks folder already exists, this will not run.
/// </summary>
/// <remarks>Bookmark directory is configurable. This will always use the default bookmark directory.</remarks>
/// <param name="directoryService"></param>

View file

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using API.Constants;
using API.Entities;
using Microsoft.AspNetCore.Identity;
namespace API.Data;
/// <summary>
/// New role introduced in v0.5.1. Adds the role to all users.
/// </summary>
public static class MigrateChangePasswordRoles
{
/// <summary>
/// Will not run if any users have the ChangePassword role already
/// </summary>
/// <param name="unitOfWork"></param>
/// <param name="userManager"></param>
public static async Task Migrate(IUnitOfWork unitOfWork, UserManager<AppUser> userManager)
{
var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.ChangePasswordRole);
if (usersWithRole.Count != 0) return;
var allUsers = await unitOfWork.UserRepository.GetAllUsers();
foreach (var user in allUsers)
{
await userManager.RemoveFromRoleAsync(user, "ChangePassword");
await userManager.AddToRoleAsync(user, PolicyConstants.ChangePasswordRole);
}
}
}

View file

@ -12,6 +12,7 @@ public interface IAppUserProgressRepository
Task<int> CleanupAbandonedChapters();
Task<bool> UserHasProgress(LibraryType libraryType, int userId);
Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId);
Task<bool> HasAnyProgressOnSeriesAsync(int seriesId, int userId);
}
public class AppUserProgressRepository : IAppUserProgressRepository
@ -76,6 +77,12 @@ public class AppUserProgressRepository : IAppUserProgressRepository
.AnyAsync();
}
public async Task<bool> HasAnyProgressOnSeriesAsync(int seriesId, int userId)
{
return await _context.AppUserProgresses
.AnyAsync(aup => aup.PagesRead > 0 && aup.AppUserId == userId && aup.SeriesId == seriesId);
}
public async Task<AppUserProgress> GetUserProgressAsync(int chapterId, int userId)
{
return await _context.AppUserProgresses

View file

@ -67,6 +67,7 @@ public class GenreRepository : IGenreRepository
.Where(s => libraryIds.Contains(s.LibraryId))
.SelectMany(s => s.Metadata.Genres)
.Distinct()
.OrderBy(p => p.Title)
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}

View file

@ -36,6 +36,7 @@ public interface ILibraryRepository
Task<bool> DeleteLibrary(int libraryId);
Task<IEnumerable<Library>> GetLibrariesForUserIdAsync(int userId);
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IList<int> libraryIds);
}
public class LibraryRepository : ILibraryRepository
@ -108,6 +109,13 @@ public class LibraryRepository : ILibraryRepository
.SingleAsync();
}
public async Task<IEnumerable<Library>> GetLibraryForIdsAsync(IList<int> libraryIds)
{
return await _context.Library
.Where(x => libraryIds.Contains(x.Id))
.ToListAsync();
}
public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
{
return await _context.Library

View file

@ -66,6 +66,8 @@ public class PersonRepository : IPersonRepository
.Where(s => libraryIds.Contains(s.LibraryId))
.SelectMany(s => s.Metadata.People)
.Distinct()
.OrderBy(p => p.Name)
.AsNoTracking()
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
@ -74,6 +76,7 @@ public class PersonRepository : IPersonRepository
public async Task<IList<Person>> GetAllPeople()
{
return await _context.Person
.OrderBy(p => p.Name)
.ToListAsync();
}
}

View file

@ -8,6 +8,8 @@ using API.DTOs;
using API.DTOs.CollectionTags;
using API.DTOs.Filtering;
using API.DTOs.Metadata;
using API.DTOs.ReadingLists;
using API.DTOs.Search;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
@ -21,6 +23,23 @@ using Microsoft.EntityFrameworkCore;
namespace API.Data.Repositories;
internal class RecentlyAddedSeries
{
public int LibraryId { get; init; }
public LibraryType LibraryType { get; init; }
public DateTime Created { get; init; }
public int SeriesId { get; init; }
public string SeriesName { get; init; }
public MangaFormat Format { get; init; }
public int ChapterId { get; init; }
public int VolumeId { get; init; }
public string ChapterNumber { get; init; }
public string ChapterRange { get; init; }
public string ChapterTitle { get; init; }
public bool IsSpecial { get; init; }
public int VolumeNumber { get; init; }
}
public interface ISeriesRepository
{
void Attach(Series series);
@ -39,10 +58,12 @@ public interface ISeriesRepository
/// <summary>
/// Does not add user information like progress, ratings, etc.
/// </summary>
/// <param name="userId"></param>
/// <param name="isAdmin"></param>
/// <param name="libraryIds"></param>
/// <param name="searchQuery">Series name to search for</param>
/// <param name="searchQuery"></param>
/// <returns></returns>
Task<IEnumerable<SearchResultDto>> SearchSeries(int[] libraryIds, string searchQuery);
Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery);
Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId);
Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId);
Task<bool> DeleteSeriesAsync(int seriesId);
@ -73,6 +94,8 @@ public interface ISeriesRepository
Task<IList<AgeRatingDto>> GetAllAgeRatingsDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<LanguageDto>> GetAllLanguagesForLibrariesAsync(List<int> libraryIds);
Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds);
Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId);
Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId);
}
public class SeriesRepository : ISeriesRepository
@ -124,6 +147,7 @@ public class SeriesRepository : ISeriesRepository
.CountAsync() > 1;
}
public async Task<IEnumerable<Series>> GetSeriesForLibraryIdAsync(int libraryId)
{
return await _context.Series
@ -209,15 +233,18 @@ public class SeriesRepository : ISeriesRepository
.SingleOrDefaultAsync();
}
/// <summary>
/// Gets all series
/// </summary>
/// <param name="libraryId"></param>
/// <param name="userId"></param>
/// <param name="userParams"></param>
/// <param name="filter"></param>
/// <returns></returns>
public async Task<PagedList<SeriesDto>> GetSeriesDtoForLibraryIdAsync(int libraryId, int userId, UserParams userParams, FilterDto filter)
{
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
if (filter.SortOptions == null)
{
query = query.OrderBy(s => s.SortName);
}
var retSeries = query
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
.AsSplitQuery()
@ -244,9 +271,25 @@ public class SeriesRepository : ISeriesRepository
};
}
public async Task<IEnumerable<SearchResultDto>> SearchSeries(int[] libraryIds, string searchQuery)
public async Task<SearchResultGroupDto> SearchSeries(int userId, bool isAdmin, int[] libraryIds, string searchQuery)
{
return await _context.Series
var result = new SearchResultGroupDto();
var seriesIds = _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Select(s => s.Id)
.ToList();
result.Libraries = await _context.Library
.Where(l => libraryIds.Contains(l.Id))
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%"))
.OrderBy(l => l.Name)
.AsSplitQuery()
.ProjectTo<LibraryDto>(_mapper.ConfigurationProvider)
.ToListAsync();
result.Series = await _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Where(s => EF.Functions.Like(s.Name, $"%{searchQuery}%")
|| EF.Functions.Like(s.OriginalName, $"%{searchQuery}%")
@ -254,17 +297,56 @@ public class SeriesRepository : ISeriesRepository
.Include(s => s.Library)
.OrderBy(s => s.SortName)
.AsNoTracking()
.AsSplitQuery()
.ProjectTo<SearchResultDto>(_mapper.ConfigurationProvider)
.ToListAsync();
result.ReadingLists = await _context.ReadingList
.Where(rl => rl.AppUserId == userId || rl.Promoted)
.Where(rl => EF.Functions.Like(rl.Title, $"%{searchQuery}%"))
.AsSplitQuery()
.ProjectTo<ReadingListDto>(_mapper.ConfigurationProvider)
.ToListAsync();
result.Collections = await _context.CollectionTag
.Where(s => EF.Functions.Like(s.Title, $"%{searchQuery}%")
|| EF.Functions.Like(s.NormalizedTitle, $"%{searchQuery}%"))
.Where(s => s.Promoted || isAdmin)
.OrderBy(s => s.Title)
.AsNoTracking()
.OrderBy(c => c.NormalizedTitle)
.ProjectTo<CollectionTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
result.Persons = await _context.SeriesMetadata
.Where(sm => seriesIds.Contains(sm.SeriesId))
.SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%")))
.AsSplitQuery()
.Distinct()
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
.ToListAsync();
result.Genres = await _context.SeriesMetadata
.Where(sm => seriesIds.Contains(sm.SeriesId))
.SelectMany(sm => sm.Genres.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
.AsSplitQuery()
.OrderBy(t => t.Title)
.Distinct()
.ProjectTo<GenreTagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
result.Tags = await _context.SeriesMetadata
.Where(sm => seriesIds.Contains(sm.SeriesId))
.SelectMany(sm => sm.Tags.Where(t => EF.Functions.Like(t.Title, $"%{searchQuery}%")))
.AsSplitQuery()
.OrderBy(t => t.Title)
.Distinct()
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
return result;
}
public async Task<SeriesDto> GetSeriesDtoByIdAsync(int seriesId, int userId)
{
var series = await _context.Series.Where(x => x.Id == seriesId)
@ -277,9 +359,6 @@ public class SeriesRepository : ISeriesRepository
return seriesList[0];
}
public async Task<bool> DeleteSeriesAsync(int seriesId)
{
var series = await _context.Series.Where(s => s.Id == seriesId).SingleOrDefaultAsync();
@ -345,7 +424,7 @@ public class SeriesRepository : ISeriesRepository
}
/// <summary>
/// This returns a dictonary mapping seriesId -> list of chapters back for each series id passed
/// This returns a dictionary mapping seriesId -> list of chapters back for each series id passed
/// </summary>
/// <param name="seriesIds"></param>
/// <returns></returns>
@ -452,7 +531,6 @@ public class SeriesRepository : ISeriesRepository
allPeopleIds.AddRange(filter.Publisher);
allPeopleIds.AddRange(filter.CoverArtist);
allPeopleIds.AddRange(filter.Translators);
//allPeopleIds.AddRange(filter.Artist);
hasPeopleFilter = allPeopleIds.Count > 0;
hasGenresFilter = filter.Genres.Count > 0;
@ -566,7 +644,7 @@ public class SeriesRepository : ISeriesRepository
&& (!hasPeopleFilter || s.Metadata.People.Any(p => allPeopleIds.Contains(p.Id)))
&& (!hasCollectionTagFilter ||
s.Metadata.CollectionTags.Any(t => filter.CollectionTags.Contains(t.Id)))
&& (!hasRatingFilter || s.Ratings.Any(r => r.Rating >= filter.Rating))
&& (!hasRatingFilter || s.Ratings.Any(r => r.Rating >= filter.Rating && r.AppUserId == userId))
&& (!hasProgressFilter || seriesIds.Contains(s.Id))
&& (!hasAgeRating || filter.AgeRating.Contains(s.Metadata.AgeRating))
&& (!hasTagsFilter || s.Metadata.Tags.Any(t => filter.Tags.Contains(t.Id)))
@ -575,34 +653,32 @@ public class SeriesRepository : ISeriesRepository
)
.AsNoTracking();
if (filter.SortOptions != null)
// If no sort options, default to using SortName
filter.SortOptions ??= new SortOptions()
{
if (filter.SortOptions.IsAscending)
IsAscending = true,
SortField = SortField.SortName
};
if (filter.SortOptions.IsAscending)
{
query = filter.SortOptions.SortField switch
{
if (filter.SortOptions.SortField == SortField.SortName)
{
query = query.OrderBy(s => s.SortName);
} else if (filter.SortOptions.SortField == SortField.CreatedDate)
{
query = query.OrderBy(s => s.Created);
} else if (filter.SortOptions.SortField == SortField.LastModifiedDate)
{
query = query.OrderBy(s => s.LastModified);
}
}
else
SortField.SortName => query.OrderBy(s => s.SortName),
SortField.CreatedDate => query.OrderBy(s => s.Created),
SortField.LastModifiedDate => query.OrderBy(s => s.LastModified),
_ => query
};
}
else
{
query = filter.SortOptions.SortField switch
{
if (filter.SortOptions.SortField == SortField.SortName)
{
query = query.OrderByDescending(s => s.SortName);
} else if (filter.SortOptions.SortField == SortField.CreatedDate)
{
query = query.OrderByDescending(s => s.Created);
} else if (filter.SortOptions.SortField == SortField.LastModifiedDate)
{
query = query.OrderByDescending(s => s.LastModified);
}
}
SortField.SortName => query.OrderByDescending(s => s.SortName),
SortField.CreatedDate => query.OrderByDescending(s => s.Created),
SortField.LastModifiedDate => query.OrderByDescending(s => s.LastModified),
_ => query
};
}
return query;
@ -777,6 +853,7 @@ public class SeriesRepository : ISeriesRepository
var ret = await _context.Series
.Where(s => libraryIds.Contains(s.LibraryId))
.Select(s => s.Metadata.Language)
.AsNoTracking()
.Distinct()
.ToListAsync();
@ -786,7 +863,9 @@ public class SeriesRepository : ISeriesRepository
{
Title = CultureInfo.GetCultureInfo(s).DisplayName,
IsoCode = s
}).ToList();
})
.OrderBy(s => s.Title)
.ToList();
}
public async Task<IList<PublicationStatusDto>> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds)
@ -800,6 +879,154 @@ public class SeriesRepository : ISeriesRepository
Value = s,
Title = s.ToDescription()
})
.OrderBy(s => s.Title)
.ToListAsync();
}
private static string RecentlyAddedItemTitle(RecentlyAddedSeries item)
{
switch (item.LibraryType)
{
case LibraryType.Book:
return string.Empty;
case LibraryType.Comic:
return "Issue";
case LibraryType.Manga:
default:
return "Chapter";
}
}
/// <summary>
/// Show all recently added chapters. Provide some mapping for chapter 0 -> Volume 1
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<IList<RecentlyAddedItemDto>> GetRecentlyAddedChapters(int userId)
{
var ret = await GetRecentlyAddedChaptersQuery(userId);
var items = new List<RecentlyAddedItemDto>();
foreach (var item in ret)
{
var dto = new RecentlyAddedItemDto()
{
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = items.Count,
Format = item.Format
};
// Add title and Volume/Chapter Id
var chapterTitle = RecentlyAddedItemTitle(item);
string title;
if (item.ChapterNumber.Equals(Parser.Parser.DefaultChapter))
{
if ((item.VolumeNumber + string.Empty).Equals(Parser.Parser.DefaultChapter))
{
title = item.ChapterTitle;
}
else
{
title = "Volume " + item.VolumeNumber;
}
dto.VolumeId = item.VolumeId;
}
else
{
title = item.IsSpecial
? item.ChapterRange
: $"{chapterTitle} {item.ChapterRange}";
dto.ChapterId = item.ChapterId;
}
dto.Title = title;
items.Add(dto);
}
return items;
}
/// <summary>
/// Return recently updated series, regardless of read progress, and group the number of volume or chapters added.
/// </summary>
/// <param name="userId">Used to ensure user has access to libraries</param>
/// <returns></returns>
public async Task<IList<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId)
{
var ret = await GetRecentlyAddedChaptersQuery(userId, 150);
var seriesMap = new Dictionary<string, GroupedSeriesDto>();
var index = 0;
foreach (var item in ret)
{
if (seriesMap.ContainsKey(item.SeriesName))
{
seriesMap[item.SeriesName].Count += 1;
}
else
{
seriesMap[item.SeriesName] = new GroupedSeriesDto()
{
LibraryId = item.LibraryId,
LibraryType = item.LibraryType,
SeriesId = item.SeriesId,
SeriesName = item.SeriesName,
Created = item.Created,
Id = index,
Format = item.Format,
Count = 1
};
index += 1;
}
}
return seriesMap.Values.ToList();
}
private async Task<List<RecentlyAddedSeries>> GetRecentlyAddedChaptersQuery(int userId, int maxRecords = 50)
{
var libraries = await _context.AppUser
.Where(u => u.Id == userId)
.SelectMany(u => u.Libraries.Select(l => new {LibraryId = l.Id, LibraryType = l.Type}))
.ToListAsync();
var libraryIds = libraries.Select(l => l.LibraryId).ToList();
var withinLastWeek = DateTime.Now - TimeSpan.FromDays(12);
var ret = await _context.Chapter
.Where(c => c.Created >= withinLastWeek)
.AsNoTracking()
.Include(c => c.Volume)
.ThenInclude(v => v.Series)
.ThenInclude(s => s.Library)
.OrderByDescending(c => c.Created)
.Select(c => new RecentlyAddedSeries()
{
LibraryId = c.Volume.Series.LibraryId,
LibraryType = c.Volume.Series.Library.Type,
Created = c.Created,
SeriesId = c.Volume.Series.Id,
SeriesName = c.Volume.Series.Name,
VolumeId = c.VolumeId,
ChapterId = c.Id,
Format = c.Volume.Series.Format,
ChapterNumber = c.Number,
ChapterRange = c.Range,
IsSpecial = c.IsSpecial,
VolumeNumber = c.Volume.Number,
ChapterTitle = c.Title
})
.Take(maxRecords)
.Where(c => c.Created >= withinLastWeek && libraryIds.Contains(c.LibraryId))
.ToListAsync();
return ret;
}
}

View file

@ -50,13 +50,13 @@ public class TagRepository : ITagRepository
public async Task RemoveAllTagNoLongerAssociated(bool removeExternal = false)
{
var TagsWithNoConnections = await _context.Tag
var tagsWithNoConnections = await _context.Tag
.Include(p => p.SeriesMetadatas)
.Include(p => p.Chapters)
.Where(p => p.SeriesMetadatas.Count == 0 && p.Chapters.Count == 0 && p.ExternalTag == removeExternal)
.ToListAsync();
_context.Tag.RemoveRange(TagsWithNoConnections);
_context.Tag.RemoveRange(tagsWithNoConnections);
await _context.SaveChangesAsync();
}
@ -67,6 +67,8 @@ public class TagRepository : ITagRepository
.Where(s => libraryIds.Contains(s.LibraryId))
.SelectMany(s => s.Metadata.Tags)
.Distinct()
.OrderBy(t => t.Title)
.AsNoTracking()
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}
@ -80,6 +82,7 @@ public class TagRepository : ITagRepository
{
return await _context.Tag
.AsNoTracking()
.OrderBy(t => t.Title)
.ProjectTo<TagDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
@ -20,7 +21,8 @@ public enum AppUserIncludes
Progress = 2,
Bookmarks = 4,
ReadingLists = 8,
Ratings = 16
Ratings = 16,
UserPreferences = 32
}
public interface IUserRepository
@ -29,7 +31,9 @@ public interface IUserRepository
void Update(AppUserPreferences preferences);
void Update(AppUserBookmark bookmark);
public void Delete(AppUser user);
Task<IEnumerable<MemberDto>> GetMembersAsync();
void Delete(AppUserBookmark bookmark);
Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync();
Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync();
Task<IEnumerable<AppUser>> GetAdminUsersAsync();
Task<IEnumerable<AppUser>> GetNonAdminUsersAsync();
Task<bool> IsUserAdminAsync(AppUser user);
@ -48,6 +52,9 @@ public interface IUserRepository
Task<int> GetUserIdByUsernameAsync(string username);
Task<AppUser> GetUserWithReadingListsByUsernameAsync(string username);
Task<IList<AppUserBookmark>> GetAllBookmarksByIds(IList<int> bookmarkIds);
Task<AppUser> GetUserByEmailAsync(string email);
Task<IEnumerable<AppUser>> GetAllUsers();
}
public class UserRepository : IUserRepository
@ -83,6 +90,11 @@ public class UserRepository : IUserRepository
_context.AppUser.Remove(user);
}
public void Delete(AppUserBookmark bookmark)
{
_context.AppUserBookmark.Remove(bookmark);
}
/// <summary>
/// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags.
/// </summary>
@ -156,6 +168,13 @@ public class UserRepository : IUserRepository
query = query.Include(u => u.Ratings);
}
if (includeFlags.HasFlag(AppUserIncludes.UserPreferences))
{
query = query.Include(u => u.UserPreferences);
}
return query;
}
@ -198,6 +217,16 @@ public class UserRepository : IUserRepository
.ToListAsync();
}
public async Task<AppUser> GetUserByEmailAsync(string email)
{
return await _context.AppUser.SingleOrDefaultAsync(u => u.Email.ToLower().Equals(email.ToLower()));
}
public async Task<IEnumerable<AppUser>> GetAllUsers()
{
return await _context.AppUser.ToListAsync();
}
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
{
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
@ -280,9 +309,10 @@ public class UserRepository : IUserRepository
}
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
public async Task<IEnumerable<MemberDto>> GetEmailConfirmedMemberDtosAsync()
{
return await _context.Users
.Where(u => u.EmailConfirmed)
.Include(x => x.Libraries)
.Include(r => r.UserRoles)
.ThenInclude(r => r.Role)
@ -291,6 +321,7 @@ public class UserRepository : IUserRepository
{
Id = u.Id,
Username = u.UserName,
Email = u.Email,
Created = u.Created,
LastActive = u.LastActive,
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
@ -305,4 +336,42 @@ public class UserRepository : IUserRepository
.AsNoTracking()
.ToListAsync();
}
public async Task<IEnumerable<MemberDto>> GetPendingMemberDtosAsync()
{
return await _context.Users
.Where(u => !u.EmailConfirmed)
.Include(x => x.Libraries)
.Include(r => r.UserRoles)
.ThenInclude(r => r.Role)
.OrderBy(u => u.UserName)
.Select(u => new MemberDto
{
Id = u.Id,
Username = u.UserName,
Email = u.Email,
Created = u.Created,
LastActive = u.LastActive,
Roles = u.UserRoles.Select(r => r.Role.Name).ToList(),
Libraries = u.Libraries.Select(l => new LibraryDto
{
Name = l.Name,
Type = l.Type,
LastScanned = l.LastScanned,
Folders = l.Folders.Select(x => x.Path).ToList()
}).ToList()
})
.AsNoTracking()
.ToListAsync();
}
public async Task<bool> ValidateUserExists(string username)
{
if (await _userManager.Users.AnyAsync(x => x.NormalizedUserName == username.ToUpper()))
{
throw new ValidationException("Username is taken.");
}
return true;
}
}

View file

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
using API.DTOs;
using API.Entities;
using API.Extensions;

View file

@ -60,6 +60,7 @@ namespace API.Data
new () {Key = ServerSettingKey.InstallId, Value = HashUtil.AnonymousToken()},
new () {Key = ServerSettingKey.InstallVersion, Value = BuildInfo.Version.ToString()},
new () {Key = ServerSettingKey.BookmarkDirectory, Value = directoryService.BookmarkDirectory},
new () {Key = ServerSettingKey.EmailServiceUrl, Value = EmailService.DefaultApiUrl},
};
foreach (var defaultSetting in DefaultSettings)
@ -92,12 +93,9 @@ namespace API.Data
await context.Database.EnsureCreatedAsync();
var users = await context.AppUser.ToListAsync();
foreach (var user in users)
foreach (var user in users.Where(user => string.IsNullOrEmpty(user.ApiKey)))
{
if (string.IsNullOrEmpty(user.ApiKey))
{
user.ApiKey = HashUtil.ApiKey();
}
user.ApiKey = HashUtil.ApiKey();
}
await context.SaveChangesAsync();
}