using System.Net; using FluentValidation.Results; using Hutopy.Web.Common.Security; using Hutopy.Web.Features.Contents.Data; using Microsoft.Extensions.Options; using Npgsql; namespace Hutopy.Web.Features.Contents.Handlers; [PublicAPI] public record ReserveSlugRequest { public string Slug { get; set; } = null!; public required Guid ReservationId { get; set; } } [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( ContentDbContext context, IOptions opts) : Endpoint { public override void Configure() { Post("/api/creators/@{Slug}/reserve"); Options(o => o.WithTags("Contents")); } public override async Task HandleAsync( ReserveSlugRequest req, CancellationToken ct) { await using var transaction = await context.Database.BeginTransactionAsync(ct); try { await context.Slugs.AddAsync( new Slugs { Id = req.ReservationId, Active = false, Name = req.Slug, ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration, CreatedBy = User.GetUserId(), }, cancellationToken: ct); 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)); } } } }