Files
social-media/backend/Modules/Creators/Features/ReserveSlug.cs

110 lines
3.2 KiB
C#

using System.Net;
using FluentValidation.Results;
using Hutopy.Infrastructure.Security;
using Hutopy.Modules.Creators.Configuration;
using Hutopy.Modules.Creators.Data;
using Hutopy.Modules.Creators.Services;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Options;
using Npgsql;
namespace Hutopy.Modules.Creators.Features;
[PublicAPI]
public record ReserveSlugRequest
{
public required Guid ReservationId { get; set; }
public string Slug { get; set; } = null!;
}
[PublicAPI]
public sealed class ReserveSlugRequestValidator : Validator<ReserveSlugRequest>
{
public ReserveSlugRequestValidator()
{
RuleFor(r => r.Slug)
.NotEmpty()
.NotNull()
.WithMessage("You should specify a valid Slug");
}
}
[PublicAPI]
public sealed class ReserveSlug(
CreatorsDbContext context,
IOptions<CreatorOptions> opts,
SlugPurger slugPurger)
: Endpoint<ReserveSlugRequest>
{
public override void Configure()
{
Post("/api/creators/@{Slug}/reserve");
Options(o => o.WithTags("Creators"));
}
public override async Task HandleAsync(
ReserveSlugRequest req,
CancellationToken ct)
{
await using IDbContextTransaction transaction = await context.Database.BeginTransactionAsync(ct);
try
{
// First, purge any expired slugs
await slugPurger.PurgeExpiredSlugsAsync(ct);
Slugs? reservation = await context.Slugs.FirstOrDefaultAsync(
s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
ct);
if (reservation == null)
{
reservation = new Slugs
{
Id = req.ReservationId, CreatedBy = User.GetUserId(), CreatedAt = DateTimeOffset.UtcNow
};
context.Slugs.Attach(reservation);
context.Entry(reservation).State = EntityState.Added;
}
reservation.Name = req.Slug;
reservation.ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration;
await context.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
await SendOkAsync(new { Message = "Slug reserved." }, ct);
}
catch (Exception e)
{
await transaction.RollbackAsync(ct);
Logger.LogError("Transaction failed: {Message}", e.Message);
if (e.InnerException is PostgresException innerException)
{
if (innerException.ConstraintName == "IX_Slugs_NormalizedName")
{
await SendResultAsync(new ProblemDetails(
[
new ValidationFailure(nameof(Slugs.Name),
"The name is already taken.")
],
(int)HttpStatusCode.Conflict));
}
}
else
{
await SendResultAsync(new ProblemDetails(
[
new ValidationFailure(nameof(Slugs.Name),
e.Message)
],
(int)HttpStatusCode.Conflict));
}
}
}
}