Fix query and API bugs: 403 for non-owners, page bounds, lifecycle filters, cover fallback, race-safe inserts, async album delete
This commit is contained in:
@@ -3,6 +3,7 @@ using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Events;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
@@ -31,7 +32,7 @@ public class DeleteTrackCommandHandler(
|
||||
if (track.OwnerUserId != request.UserId)
|
||||
{
|
||||
logger.LogWarning("User {UserId} unauthorized to delete track {TrackId}", request.UserId, request.TrackId);
|
||||
return Error.Unauthorized();
|
||||
return AppErrors.Forbidden("Track", request.TrackId);
|
||||
}
|
||||
|
||||
if (track.Audio.IsInProgress)
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks;
|
||||
|
||||
public record GetLastTrackListenedByUserIdQuery(long UserId) : IQuery<ErrorOr<TrackApplicationResponse>>;
|
||||
public record GetLastTrackListenedByUserIdQuery(long UserId) : IQuery<TrackApplicationResponse?>;
|
||||
|
||||
public class GetLastTrackListenedByUserIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetLastTrackListenedByUserIdQuery, ErrorOr<TrackApplicationResponse>>
|
||||
: IQueryHandler<GetLastTrackListenedByUserIdQuery, TrackApplicationResponse?>
|
||||
{
|
||||
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetLastTrackListenedByUserIdQuery query, CancellationToken cancellationToken)
|
||||
public async ValueTask<TrackApplicationResponse?> Handle(GetLastTrackListenedByUserIdQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var track = await database.ListeningHistories
|
||||
return await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Track.Owner)
|
||||
.Include(t => t.Track.Tags)
|
||||
.Where(t => t.UserId == query.UserId)
|
||||
.Where(t => t.UserId == query.UserId && t.IsCounted && t.Track.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.OrderByDescending(t => t.ListenedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.Track.ListeningHistories.Count(l => l.IsCounted)))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return track == null
|
||||
? Error.NotFound("Track.NotFound", "No track found for the given user ID.")
|
||||
: track;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks;
|
||||
|
||||
@@ -28,11 +29,12 @@ public class GetListeningHistoryQueryHandler(
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Owner)
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => recentTrackIds.Contains(t.Id))
|
||||
.Where(t => recentTrackIds.Contains(t.Id) && t.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.ToDictionaryAsync(t => t.Id, cancellationToken);
|
||||
|
||||
return recentTrackIds
|
||||
.Where(id => tracksById.ContainsKey(id))
|
||||
.Select(id => tracksById[id])
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.ListensCount));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks;
|
||||
|
||||
@@ -17,7 +18,7 @@ public class GetTrackByIdQueryHandler(IDatabase database)
|
||||
var entity = await database.Tracks.AsNoTracking()
|
||||
.Include(t => t.Owner)
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => t.Id == request.TrackId)
|
||||
.Where(t => t.Id == request.TrackId && t.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks;
|
||||
|
||||
@@ -26,7 +27,7 @@ public class GetTrackStreamQueryHandler(
|
||||
return Error.Unauthorized(description: "Sign in to stream music, or ask an administrator to enable anonymous listening.");
|
||||
|
||||
var track = await database.Tracks.AsNoTracking()
|
||||
.Where(t => t.Id == request.TrackId)
|
||||
.Where(t => t.Id == request.TrackId && t.LifeCycleStatus == LifeCycleStatus.Active)
|
||||
.Select(t => new { t.Id, t.Audio })
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ public class GetTracksQueryHandler(IDatabase database)
|
||||
.AsNoTracking()
|
||||
.Include(ut => ut.Track.Owner)
|
||||
.Include(ut => ut.Track.Tags)
|
||||
.AsQueryable();
|
||||
.Where(ut => ut.Track.LifeCycleStatus == LifeCycleStatus.Active);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Name))
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using Musify.Application.Shared;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
using X.PagedList;
|
||||
using X.PagedList.EF;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks;
|
||||
|
||||
@@ -21,14 +22,11 @@ public class GetTracksByUserIdQueryHandler(IDatabase database)
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber;
|
||||
var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
|
||||
|
||||
var tracksQuery = database.UserHasTracks
|
||||
.AsNoTracking()
|
||||
.Include(ut => ut.Track.Owner)
|
||||
.Include(ut => ut.Track.Tags)
|
||||
.Where(p => p.UserId == request.UserId);
|
||||
.Where(p => p.UserId == request.UserId && p.Track.LifeCycleStatus == LifeCycleStatus.Active);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.Name))
|
||||
{
|
||||
@@ -41,7 +39,7 @@ public class GetTracksByUserIdQueryHandler(IDatabase database)
|
||||
var pagedEntities = await tracksQuery
|
||||
.OrderBy(t => t.Track.CreatedAt)
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
pagedEntities.Select(x => TrackApplicationResponse.FromEntity(x.Track, x.ListensCount)),
|
||||
|
||||
Reference in New Issue
Block a user