using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Constants; using API.DTOs; using API.DTOs.Account; using API.DTOs.Filtering; using API.DTOs.Reader; using API.DTOs.Scrobbling; using API.DTOs.SeriesDetail; using API.Entities; using API.Extensions; using API.Extensions.QueryExtensions; using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; [Flags] public enum AppUserIncludes { None = 1, Progress = 2, Bookmarks = 4, ReadingLists = 8, Ratings = 16, UserPreferences = 32, WantToRead = 64, ReadingListsWithItems = 128, Devices = 256, ScrobbleHolds = 512 } public interface IUserRepository { void Update(AppUser user); void Update(AppUserPreferences preferences); void Update(AppUserBookmark bookmark); void Add(AppUserBookmark bookmark); public void Delete(AppUser? user); void Delete(AppUserBookmark bookmark); Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true); Task> GetAdminUsersAsync(); Task IsUserAdminAsync(AppUser? user); Task GetUserRatingAsync(int seriesId, int userId); Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId); Task GetPreferencesAsync(string username); Task> GetBookmarkDtosForSeries(int userId, int seriesId); Task> GetBookmarkDtosForVolume(int userId, int volumeId); Task> GetBookmarkDtosForChapter(int userId, int chapterId); Task> GetAllBookmarkDtos(int userId, FilterDto filter); Task> GetAllBookmarksAsync(); Task GetBookmarkForPage(int page, int chapterId, int userId); Task GetBookmarkAsync(int bookmarkId); Task GetUserIdByApiKeyAsync(string apiKey); Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None); Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None); Task GetUserIdByUsernameAsync(string username); Task> GetAllBookmarksByIds(IList bookmarkIds); Task GetUserByEmailAsync(string email); Task> GetAllPreferencesByThemeAsync(int themeId); Task HasAccessToLibrary(int libraryId, int userId); Task HasAccessToSeries(int userId, int seriesId); Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None); Task GetUserByConfirmationToken(string token); Task GetDefaultAdminUser(); Task> GetSeriesWithRatings(int userId); Task> GetSeriesWithReviews(int userId); Task HasHoldOnSeries(int userId, int seriesId); Task> GetHolds(int userId); } public class UserRepository : IUserRepository { private readonly DataContext _context; private readonly UserManager _userManager; private readonly IMapper _mapper; public UserRepository(DataContext context, UserManager userManager, IMapper mapper) { _context = context; _userManager = userManager; _mapper = mapper; } public void Update(AppUser user) { _context.Entry(user).State = EntityState.Modified; } public void Update(AppUserPreferences preferences) { _context.Entry(preferences).State = EntityState.Modified; } public void Update(AppUserBookmark bookmark) { _context.Entry(bookmark).State = EntityState.Modified; } public void Add(AppUserBookmark bookmark) { _context.AppUserBookmark.Add(bookmark); } public void Delete(AppUser? user) { if (user == null) return; _context.AppUser.Remove(user); } public void Delete(AppUserBookmark bookmark) { _context.AppUserBookmark.Remove(bookmark); } /// /// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags. /// /// /// Includes() you want. Pass multiple with flag1 | flag2 /// public async Task GetUserByUsernameAsync(string username, AppUserIncludes includeFlags = AppUserIncludes.None) { return await _context.Users .Where(x => x.UserName == username) .Includes(includeFlags) .SingleOrDefaultAsync(); } /// /// A one stop shop to get a tracked AppUser instance with any number of JOINs generated by passing bitwise flags. /// /// /// Includes() you want. Pass multiple with flag1 | flag2 /// public async Task GetUserByIdAsync(int userId, AppUserIncludes includeFlags = AppUserIncludes.None) { return await _context.Users .Where(x => x.Id == userId) .Includes(includeFlags) .FirstOrDefaultAsync(); } public async Task> GetAllBookmarksAsync() { return await _context.AppUserBookmark.ToListAsync(); } public async Task GetBookmarkForPage(int page, int chapterId, int userId) { return await _context.AppUserBookmark .Where(b => b.Page == page && b.ChapterId == chapterId && b.AppUserId == userId) .SingleOrDefaultAsync(); } public async Task GetBookmarkAsync(int bookmarkId) { return await _context.AppUserBookmark .Where(b => b.Id == bookmarkId) .SingleOrDefaultAsync(); } /// /// This fetches the Id for a user. Use whenever you just need an ID. /// /// /// public async Task GetUserIdByUsernameAsync(string username) { return await _context.Users .Where(x => x.UserName == username) .Select(u => u.Id) .SingleOrDefaultAsync(); } /// /// Returns all Bookmarks for a given set of Ids /// /// /// public async Task> GetAllBookmarksByIds(IList bookmarkIds) { return await _context.AppUserBookmark .Where(b => bookmarkIds.Contains(b.Id)) .OrderBy(b => b.Created) .ToListAsync(); } public async Task GetUserByEmailAsync(string email) { var lowerEmail = email.ToLower(); return await _context.AppUser.SingleOrDefaultAsync(u => u.Email != null && u.Email.ToLower().Equals(lowerEmail)); } public async Task> GetAllPreferencesByThemeAsync(int themeId) { return await _context.AppUserPreferences .Include(p => p.Theme) .Where(p => p.Theme.Id == themeId) .AsSplitQuery() .ToListAsync(); } public async Task HasAccessToLibrary(int libraryId, int userId) { return await _context.Library .Include(l => l.AppUsers) .AsSplitQuery() .AnyAsync(library => library.AppUsers.Any(user => user.Id == userId) && library.Id == libraryId); } /// /// Does the user have library and age restriction access to a given series /// /// public async Task HasAccessToSeries(int userId, int seriesId) { var userRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Series .Include(s => s.Library) .Where(s => s.Library.AppUsers.Any(user => user.Id == userId)) .RestrictAgainstAgeRestriction(userRating) .AsSplitQuery() .AnyAsync(s => s.Id == seriesId); } public async Task> GetAllUsersAsync(AppUserIncludes includeFlags = AppUserIncludes.None) { return await _context.AppUser .Includes(includeFlags) .ToListAsync(); } public async Task GetUserByConfirmationToken(string token) { return await _context.AppUser .SingleOrDefaultAsync(u => u.ConfirmationToken != null && u.ConfirmationToken.Equals(token)); } /// /// Returns the first admin account created /// /// public async Task GetDefaultAdminUser() { return (await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole)) .OrderBy(u => u.Created) .First(); } public async Task> GetSeriesWithRatings(int userId) { return await _context.AppUserRating .Where(u => u.AppUserId == userId && u.Rating > 0) .Include(u => u.Series) .AsSplitQuery() .ToListAsync(); } public async Task> GetSeriesWithReviews(int userId) { return await _context.AppUserRating .Where(u => u.AppUserId == userId && !string.IsNullOrEmpty(u.Review)) .Include(u => u.Series) .AsSplitQuery() .ToListAsync(); } public async Task HasHoldOnSeries(int userId, int seriesId) { return await _context.AppUser .AsSplitQuery() .AnyAsync(u => u.ScrobbleHolds.Select(s => s.SeriesId).Contains(seriesId) && u.Id == userId); } public async Task> GetHolds(int userId) { return await _context.ScrobbleHold .Where(s => s.AppUserId == userId) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } public async Task> GetAdminUsersAsync() { return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); } public async Task IsUserAdminAsync(AppUser? user) { if (user == null) return false; return await _userManager.IsInRoleAsync(user, PolicyConstants.AdminRole); } public async Task GetUserRatingAsync(int seriesId, int userId) { return await _context.AppUserRating .Where(r => r.SeriesId == seriesId && r.AppUserId == userId) .SingleOrDefaultAsync(); } public async Task> GetUserRatingDtosForSeriesAsync(int seriesId, int userId) { return await _context.AppUserRating .Include(r => r.AppUser) .Where(r => r.SeriesId == seriesId) .Where(r => r.AppUser.UserPreferences.ShareReviews || r.AppUserId == userId) .OrderBy(r => r.AppUserId == userId) .ThenBy(r => r.Rating) .AsSplitQuery() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } public async Task GetPreferencesAsync(string username) { return await _context.AppUserPreferences .Include(p => p.AppUser) .Include(p => p.Theme) .AsSplitQuery() .SingleOrDefaultAsync(p => p.AppUser.UserName == username); } public async Task> GetBookmarkDtosForSeries(int userId, int seriesId) { return await _context.AppUserBookmark .Where(x => x.AppUserId == userId && x.SeriesId == seriesId) .OrderBy(x => x.Created) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } public async Task> GetBookmarkDtosForVolume(int userId, int volumeId) { return await _context.AppUserBookmark .Where(x => x.AppUserId == userId && x.VolumeId == volumeId) .OrderBy(x => x.Created) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } public async Task> GetBookmarkDtosForChapter(int userId, int chapterId) { return await _context.AppUserBookmark .Where(x => x.AppUserId == userId && x.ChapterId == chapterId) .OrderBy(x => x.Created) .AsNoTracking() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } /// /// Get all bookmarks for the user /// /// /// Only supports SeriesNameQuery /// public async Task> GetAllBookmarkDtos(int userId, FilterDto filter) { var query = _context.AppUserBookmark .Where(x => x.AppUserId == userId) .OrderBy(x => x.Created) .AsNoTracking(); if (string.IsNullOrEmpty(filter.SeriesNameQuery)) return await query .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); var seriesNameQueryNormalized = filter.SeriesNameQuery.ToNormalized(); var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new { bookmark, series }) .Where(o => (EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%")) || (o.series.OriginalName != null && EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%")) || (o.series.LocalizedName != null && EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%")) || (EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%")) ); query = filterSeriesQuery.Select(o => o.bookmark); return await query .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } /// /// Fetches the UserId by API Key. This does not include any extra information /// /// /// public async Task GetUserIdByApiKeyAsync(string apiKey) { return await _context.AppUser .Where(u => u.ApiKey != null && u.ApiKey.Equals(apiKey)) .Select(u => u.Id) .FirstOrDefaultAsync(); } public async Task> GetEmailConfirmedMemberDtosAsync(bool emailConfirmed = true) { return await _context.Users .Where(u => (emailConfirmed && u.EmailConfirmed) || !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(), IsPending = !u.EmailConfirmed, AgeRestriction = new AgeRestrictionDto() { AgeRating = u.AgeRestriction, IncludeUnknowns = u.AgeRestrictionIncludeUnknowns }, 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() }) .AsSplitQuery() .AsNoTracking() .ToListAsync(); } }