Add track coauthors with invitations, acceptance and coauthor-aware track responses
Owners invite existing users to coauthor a track; invitees accept or reject, and either side can remove a coauthor. Only accepted coauthors appear in track responses and in the coauthor's public track list. The owner also receives the full invitation list with statuses on the user tracks response.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
namespace Musify.Api.DataTransferObjects.Tracks
|
||||
{
|
||||
public record InviteCoauthorsRequest(IReadOnlyList<long> UserIds);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Mediator;
|
||||
using Musify.Api.Authentication;
|
||||
using Musify.Api.Extensions;
|
||||
using Musify.Application.Coauthors;
|
||||
|
||||
namespace Musify.Api.Endpoints
|
||||
{
|
||||
public static class CoauthorEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapCoauthorEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/coauthor-invitations")
|
||||
.WithTags("Coauthors")
|
||||
.RequireAuthorization();
|
||||
|
||||
group.MapPost("/{id:guid}/accept", AcceptInvitation)
|
||||
.WithName("AcceptCoauthorInvitation")
|
||||
.WithSummary("Accept a pending coauthor invitation addressed to you")
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status409Conflict);
|
||||
|
||||
group.MapPost("/{id:guid}/reject", RejectInvitation)
|
||||
.WithName("RejectCoauthorInvitation")
|
||||
.WithSummary("Reject a pending coauthor invitation addressed to you")
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status409Conflict);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<IResult> AcceptInvitation(
|
||||
IMediator mediator,
|
||||
CurrentUser currentUser,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new RespondToCoauthorInvitationCommand(currentUser.RequiredId, id, true), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> RejectInvitation(
|
||||
IMediator mediator,
|
||||
CurrentUser currentUser,
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new RespondToCoauthorInvitationCommand(currentUser.RequiredId, id, false), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Musify.Api.DataTransferObjects.Tracks;
|
||||
using Musify.Api.Extensions;
|
||||
using Musify.Api.Filters;
|
||||
using Musify.Api.Models;
|
||||
using Musify.Application.Coauthors;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Pictures;
|
||||
using Musify.Application.Shared;
|
||||
@@ -121,6 +122,27 @@ namespace Musify.Api.Endpoints
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden);
|
||||
|
||||
group.MapPost("/{id:guid}/coauthors", InviteCoauthors)
|
||||
.WithName("InviteCoauthors")
|
||||
.WithSummary("Invite users to coauthor one of your tracks. Returns every invitation of the track with its status. A rejected invitation is reset to pending")
|
||||
.AddEndpointFilter<ValidationFilter<InviteCoauthorsRequest>>()
|
||||
.RequireAuthorization()
|
||||
.Produces<IReadOnlyList<TrackCoauthorInviteReference>>()
|
||||
.ProducesValidationProblem()
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces(StatusCodes.Status409Conflict);
|
||||
|
||||
group.MapDelete("/{id:guid}/coauthors/{userId:long}", RemoveCoauthor)
|
||||
.WithName("RemoveCoauthor")
|
||||
.WithSummary("Remove a coauthor or pending invitation from a track. Allowed to the track owner and to the coauthor themself")
|
||||
.RequireAuthorization()
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapPost("/upload-urls", RequestTrackUploadUrls)
|
||||
.WithName("RequestTrackUploadUrls")
|
||||
.WithSummary("Request pre-signed URLs to upload the audio of one or many tracks and an optional picture shared by all of them")
|
||||
@@ -198,14 +220,37 @@ namespace Musify.Api.Endpoints
|
||||
[AsParameters] PageQuery page,
|
||||
CancellationToken cancellationToken,
|
||||
ProcessingStatus[]? status = null,
|
||||
bool? published = null)
|
||||
bool? published = null,
|
||||
bool? includeCoauthored = null)
|
||||
{
|
||||
var result = await mediator.Send(
|
||||
new GetTracksByUserIdQuery(userId, name, page.PageNumber, page.PageSize, status, currentUser.Id, published),
|
||||
new GetTracksByUserIdQuery(userId, name, page.PageNumber, page.PageSize, status, currentUser.Id, published, includeCoauthored),
|
||||
cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> InviteCoauthors(
|
||||
IMediator mediator,
|
||||
CurrentUser currentUser,
|
||||
Guid id,
|
||||
InviteCoauthorsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new InviteCoauthorsCommand(currentUser.RequiredId, id, request.UserIds), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> RemoveCoauthor(
|
||||
IMediator mediator,
|
||||
CurrentUser currentUser,
|
||||
Guid id,
|
||||
long userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new RemoveCoauthorCommand(currentUser.RequiredId, id, userId), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> SetTrackPublished(
|
||||
IMediator mediator,
|
||||
CurrentUser currentUser,
|
||||
|
||||
@@ -3,6 +3,8 @@ using Musify.Api.Authentication;
|
||||
using Musify.Api.Extensions;
|
||||
using Musify.Api.Filters;
|
||||
using Musify.Api.Models;
|
||||
using Musify.Application.Coauthors;
|
||||
using Musify.Application.Coauthors.Responses;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Tracks;
|
||||
using Musify.Application.Tracks.Responses;
|
||||
@@ -24,6 +26,14 @@ namespace Musify.Api.Endpoints
|
||||
.WithSummary("Get paginated users")
|
||||
.Produces<PaginatedResponse<UserSummaryResponse>>();
|
||||
|
||||
group.MapGet("/me/coauthor-invitations", GetCoauthorInvitations)
|
||||
.AddEndpointFilter<ValidationFilter<PageQuery>>()
|
||||
.WithName("GetCoauthorInvitations")
|
||||
.WithSummary("Get your pending coauthor invitations with the track title and owner")
|
||||
.RequireAuthorization()
|
||||
.Produces<PaginatedResponse<CoauthorInvitationResponse>>()
|
||||
.Produces(StatusCodes.Status401Unauthorized);
|
||||
|
||||
group.MapGet("/{id:long}", GetUserById)
|
||||
.WithName("GetUserById")
|
||||
.WithSummary("Get a user by id")
|
||||
@@ -108,6 +118,16 @@ namespace Musify.Api.Endpoints
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetCoauthorInvitations(
|
||||
IMediator mediator,
|
||||
CurrentUser currentUser,
|
||||
[AsParameters] PageQuery page,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await mediator.Send(new GetCoauthorInvitationsQuery(currentUser.RequiredId, page.PageNumber, page.PageSize), cancellationToken);
|
||||
return Results.Ok(result);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetUserById(
|
||||
IMediator mediator,
|
||||
long id,
|
||||
|
||||
@@ -45,6 +45,7 @@ app.UseAuthorization();
|
||||
|
||||
app
|
||||
.MapAlbumEndpoints()
|
||||
.MapCoauthorEndpoints()
|
||||
.MapConfigEndpoints()
|
||||
.MapGenreEndpoints()
|
||||
.MapLikeEndpoints()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using FluentValidation;
|
||||
using Musify.Api.DataTransferObjects.Tracks;
|
||||
|
||||
namespace Musify.Api.Validators.Tracks
|
||||
{
|
||||
public sealed class InviteCoauthorsRequestValidator : AbstractValidator<InviteCoauthorsRequest>
|
||||
{
|
||||
public const int MaxUsersPerRequest = 20;
|
||||
|
||||
public InviteCoauthorsRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.UserIds)
|
||||
.NotEmpty()
|
||||
.Must(ids => ids.Count <= MaxUsersPerRequest)
|
||||
.WithMessage($"At most {MaxUsersPerRequest} users can be invited at once.");
|
||||
|
||||
RuleForEach(x => x.UserIds)
|
||||
.GreaterThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user