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 { public ReserveSlugRequestValidator() { RuleFor(r => r.Slug) .NotEmpty() .NotNull() .WithMessage("You should specify a valid Slug"); } } [PublicAPI] public sealed class ReserveSlug( CreatorsDbContext context, IOptions opts, SlugPurger slugPurger) : Endpoint { 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)); } } } }