diff --git a/backend/src/Web/Features/Contents/Data/Creator.cs b/backend/src/Web/Features/Contents/Data/Creator.cs index fd29df8..4eaf646 100644 --- a/backend/src/Web/Features/Contents/Data/Creator.cs +++ b/backend/src/Web/Features/Contents/Data/Creator.cs @@ -18,7 +18,8 @@ public class Creator public bool AcceptDonation { get; set; } public bool Verified { get; set; } - public Slugs Slugs { get; set; } = null!; + [MaxLength(255)] public string Name { get; set; } + [MaxLength(128)] public string Slug { get; set; } [MaxLength(255)] public string? Title { get; set; } public Socials Socials { get; set; } = new(); public Images Images { get; set; } = new(); diff --git a/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.Designer.cs b/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.Designer.cs new file mode 100644 index 0000000..a252da1 --- /dev/null +++ b/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.Designer.cs @@ -0,0 +1,382 @@ +// +using System; +using Hutopy.Web.Features.Contents.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Hutopy.Web.Features.Contents.Data.Migrations +{ + [DbContext(typeof(ContentDbContext))] + [Migration("20250415071053_SplitSlugFromCreator")] + partial class SplitSlugFromCreator + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Content") + .HasAnnotation("ProductVersion", "9.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("HtmlFileUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ThumbnailUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.PrimitiveCollection("Urls") + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Contents", "Content"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AcceptDonation") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("boolean") + .HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Verified") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Creators", "Content"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Slugs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("NormalizedName") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComputedColumnSql("LOWER( \"Content\".\"Slugs\".\"Name\")", true); + + b.Property("ReservedUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("UsedBy") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique(); + + b.ToTable("Slugs", "Content"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b => + { + b.HasOne("Hutopy.Web.Features.Contents.Data.Creator", "Creator") + .WithMany() + .HasForeignKey("CreatedBy"); + + b.OwnsMany("Hutopy.Web.Features.Contents.Data.ContentReaction", "Reactions", b1 => + { + b1.Property("ContentId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Reaction") + .HasColumnType("integer"); + + b1.Property("UserId") + .HasColumnType("uuid"); + + b1.Property("UserName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.HasKey("ContentId", "Id"); + + b1.ToTable("Reactions", "Content"); + + b1.WithOwner() + .HasForeignKey("ContentId"); + }); + + b.Navigation("Creator"); + + b.Navigation("Reactions"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b => + { + b.OwnsOne("Hutopy.Web.Features.Contents.Data.Images", "Images", b1 => + { + b1.Property("CreatorId") + .HasColumnType("uuid"); + + b1.Property("Banner") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("Logo") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.HasKey("CreatorId"); + + b1.ToTable("Images", "Content"); + + b1.WithOwner() + .HasForeignKey("CreatorId"); + }); + + b.OwnsOne("Hutopy.Web.Features.Contents.Data.PresentationInfos", "PresentationInfos", b1 => + { + b1.Property("CreatorId") + .HasColumnType("uuid"); + + b1.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("Image1Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("Image2Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("Image3Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("Image4Url") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("ImagesSubtitle") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b1.Property("ImagesText") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b1.Property("MainImageText") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b1.Property("MainImageUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("MainVideoText") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b1.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("Title") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b1.Property("VideoSubtitle") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b1.Property("VideoSubtitleMain") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b1.Property("VideoText") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b1.Property("VideoUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("VideoUrlMain") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.HasKey("CreatorId"); + + b1.ToTable("PresentationInfos", "Content"); + + b1.WithOwner() + .HasForeignKey("CreatorId"); + }); + + b.OwnsOne("Hutopy.Web.Features.Contents.Data.Socials", "Socials", b1 => + { + b1.Property("CreatorId") + .HasColumnType("uuid"); + + b1.Property("FacebookUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("InstagramUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("LinkedInUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("RedditUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("TikTokUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("WebsiteUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("XUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("YoutubeUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.HasKey("CreatorId"); + + b1.ToTable("Socials", "Content"); + + b1.WithOwner() + .HasForeignKey("CreatorId"); + }); + + b.Navigation("Images") + .IsRequired(); + + b.Navigation("PresentationInfos") + .IsRequired(); + + b.Navigation("Socials") + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.cs b/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.cs new file mode 100644 index 0000000..4ecee0a --- /dev/null +++ b/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.cs @@ -0,0 +1,111 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Web.Features.Contents.Data.Migrations +{ + /// + public partial class SplitSlugFromCreator : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Creators_Slugs_SlugsId", + schema: "Content", + table: "Creators"); + + migrationBuilder.DropIndex( + name: "IX_Creators_SlugsId", + schema: "Content", + table: "Creators"); + + migrationBuilder.DropColumn( + name: "Active", + schema: "Content", + table: "Slugs"); + + migrationBuilder.DropColumn( + name: "SlugsId", + schema: "Content", + table: "Creators"); + + migrationBuilder.AddColumn( + name: "UsedBy", + schema: "Content", + table: "Slugs", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Name", + schema: "Content", + table: "Creators", + type: "character varying(255)", + maxLength: 255, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Slug", + schema: "Content", + table: "Creators", + type: "character varying(128)", + maxLength: 128, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UsedBy", + schema: "Content", + table: "Slugs"); + + migrationBuilder.DropColumn( + name: "Name", + schema: "Content", + table: "Creators"); + + migrationBuilder.DropColumn( + name: "Slug", + schema: "Content", + table: "Creators"); + + migrationBuilder.AddColumn( + name: "Active", + schema: "Content", + table: "Slugs", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SlugsId", + schema: "Content", + table: "Creators", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateIndex( + name: "IX_Creators_SlugsId", + schema: "Content", + table: "Creators", + column: "SlugsId"); + + migrationBuilder.AddForeignKey( + name: "FK_Creators_Slugs_SlugsId", + schema: "Content", + table: "Creators", + column: "SlugsId", + principalSchema: "Content", + principalTable: "Slugs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/src/Web/Features/Contents/Data/Migrations/ContentDbContextModelSnapshot.cs b/backend/src/Web/Features/Contents/Data/Migrations/ContentDbContextModelSnapshot.cs index c6adf85..410d562 100644 --- a/backend/src/Web/Features/Contents/Data/Migrations/ContentDbContextModelSnapshot.cs +++ b/backend/src/Web/Features/Contents/Data/Migrations/ContentDbContextModelSnapshot.cs @@ -97,8 +97,15 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations .HasColumnType("boolean") .HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true); - b.Property("SlugsId") - .HasColumnType("uuid"); + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Title") .HasMaxLength(255) @@ -109,8 +116,6 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations b.HasKey("Id"); - b.HasIndex("SlugsId"); - b.ToTable("Creators", "Content"); }); @@ -120,9 +125,6 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("Active") - .HasColumnType("boolean"); - b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -144,6 +146,9 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations b.Property("ReservedUntil") .HasColumnType("timestamp with time zone"); + b.Property("UsedBy") + .HasColumnType("uuid"); + b.HasKey("Id"); b.HasIndex("NormalizedName") @@ -195,12 +200,6 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b => { - b.HasOne("Hutopy.Web.Features.Contents.Data.Slugs", "Slugs") - .WithMany() - .HasForeignKey("SlugsId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - b.OwnsOne("Hutopy.Web.Features.Contents.Data.Images", "Images", b1 => { b1.Property("CreatorId") @@ -371,8 +370,6 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations b.Navigation("PresentationInfos") .IsRequired(); - b.Navigation("Slugs"); - b.Navigation("Socials") .IsRequired(); }); diff --git a/backend/src/Web/Features/Contents/Data/Slugs.cs b/backend/src/Web/Features/Contents/Data/Slugs.cs index 8925dcf..803de2d 100644 --- a/backend/src/Web/Features/Contents/Data/Slugs.cs +++ b/backend/src/Web/Features/Contents/Data/Slugs.cs @@ -7,8 +7,8 @@ public class Slugs public Guid Id { get; set; } public Guid CreatedBy { get; set; } public DateTimeOffset CreatedAt { get; init; } + public Guid? UsedBy { get; set; } [MaxLength(128)] public string Name { get; set; } = null!; [MaxLength(128)] public string NormalizedName { get; set; } = null!; public DateTimeOffset ReservedUntil { get; set; } - public bool Active { get; set; } } diff --git a/backend/src/Web/Features/Contents/Handlers/CreateContentFromHtml.cs b/backend/src/Web/Features/Contents/Handlers/CreateContentFromHtml.cs index 5b93bd3..7f97f5e 100644 --- a/backend/src/Web/Features/Contents/Handlers/CreateContentFromHtml.cs +++ b/backend/src/Web/Features/Contents/Handlers/CreateContentFromHtml.cs @@ -67,7 +67,7 @@ public sealed class PostContentHtml( { Id = c.Id, CreatedBy = c.CreatedBy, - CreatedByName = c.Creator!.Slugs.Name, + CreatedByName = c.Creator.Name, CreatedByPortraitUrl = c.Creator.Images.Logo, CreatedAt = c.CreatedAt, DeletedBy = c.DeletedBy, diff --git a/backend/src/Web/Features/Contents/Handlers/CreateCreator.cs b/backend/src/Web/Features/Contents/Handlers/CreateCreator.cs index 83ae15c..285ee1c 100644 --- a/backend/src/Web/Features/Contents/Handlers/CreateCreator.cs +++ b/backend/src/Web/Features/Contents/Handlers/CreateCreator.cs @@ -16,7 +16,7 @@ public sealed class CreateCreatorRequestValidator : Validator r.SlugReservationId) .NotNull() .NotEmpty() - .WithMessage("You should specify a valid Name"); + .WithMessage("You should specify a valid SlugReservationId"); RuleFor(r => r.CreatorId) .NotNull() @@ -48,7 +48,7 @@ public sealed class CreateCreatorHandler( .Slugs .SingleAsync(s => s.Id == req.SlugReservationId, ct); - if (slug.Active + if (slug.UsedBy is not null || slug.ReservedUntil < DateTimeOffset.UtcNow || slug.CreatedBy != User.GetUserId()) { @@ -56,14 +56,15 @@ public sealed class CreateCreatorHandler( return; } - slug.Active = true; + slug.UsedBy = req.CreatorId; await context.Creators.AddAsync( new Creator { Id = req.CreatorId, CreatedBy = User.GetUserId(), - Slugs = slug + Name = slug.Name, + Slug = slug.NormalizedName, }, ct); diff --git a/backend/src/Web/Features/Contents/Handlers/GetContent.cs b/backend/src/Web/Features/Contents/Handlers/GetContent.cs index beaa814..705d916 100644 --- a/backend/src/Web/Features/Contents/Handlers/GetContent.cs +++ b/backend/src/Web/Features/Contents/Handlers/GetContent.cs @@ -31,7 +31,7 @@ public class GetContent( { Id = c.Id, CreatedBy = c.CreatedBy, - CreatedByName = c.Creator!.Slugs.Name, + CreatedByName = c.Creator.Name, CreatedByPortraitUrl = c.Creator.Images.Logo, CreatedAt = c.CreatedAt, DeletedBy = c.DeletedBy, diff --git a/backend/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs b/backend/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs index 78e09fb..ab6051a 100644 --- a/backend/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs +++ b/backend/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs @@ -42,7 +42,7 @@ public class GetContentsByCreatorHandler( { Id = c.Id, CreatedBy = c.CreatedBy, - CreatedByName = c.Creator!.Slugs.Name, + CreatedByName = c.Creator.Name, CreatedByPortraitUrl = c.Creator.Images.Logo, CreatedAt = c.CreatedAt, DeletedBy = c.DeletedBy, diff --git a/backend/src/Web/Features/Contents/Handlers/GetCreatorBySlug.cs b/backend/src/Web/Features/Contents/Handlers/GetCreatorBySlug.cs index f1d17c0..ba4ba0f 100644 --- a/backend/src/Web/Features/Contents/Handlers/GetCreatorBySlug.cs +++ b/backend/src/Web/Features/Contents/Handlers/GetCreatorBySlug.cs @@ -65,7 +65,7 @@ public class GetCreatorBySlugHandler( var creator = await context .Creators - .Where(c => EF.Functions.ILike(c.Slugs.Name, creatorName)) + .Where(c => EF.Functions.ILike(c.Slug, creatorName)) .AsNoTracking() .Select(c => new GetCreatorBySlugResponse ( @@ -74,7 +74,7 @@ public class GetCreatorBySlugHandler( c.CreatedAt, c.Verified, c.AcceptDonation, - c.Slugs.NormalizedName, + c.Name, c.Title, c.Socials, c.PresentationInfos, diff --git a/backend/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs b/backend/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs index dbb2205..1add899 100644 --- a/backend/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs +++ b/backend/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs @@ -42,8 +42,8 @@ public class GetCreatorProfileHandler( Id = c.Id, CreatedBy = c.CreatedBy, CreatedAt = c.CreatedAt, + Name = c.Name, Title = c.Title, - Name = c.Slugs.NormalizedName, Verified = c.Verified, AcceptDonation = c.AcceptDonation, Images = c.Images, diff --git a/backend/src/Web/Features/Contents/Handlers/GetFeaturedContents.cs b/backend/src/Web/Features/Contents/Handlers/GetFeaturedContents.cs index f213b9c..30cfc70 100644 --- a/backend/src/Web/Features/Contents/Handlers/GetFeaturedContents.cs +++ b/backend/src/Web/Features/Contents/Handlers/GetFeaturedContents.cs @@ -41,7 +41,7 @@ public class GetFeaturedContentsHandler( { Id = c.Id, CreatedBy = c.CreatedBy, - CreatedByName = c.Creator!.Slugs.Name, + CreatedByName = c.Creator.Name, CreatedByPortraitUrl = c.Creator.Images.Logo, CreatedAt = c.CreatedAt, DeletedBy = c.DeletedBy, diff --git a/backend/src/Web/Features/Contents/Handlers/RemoveCreator.cs b/backend/src/Web/Features/Contents/Handlers/RemoveCreator.cs index b3f3030..fc31ea2 100644 --- a/backend/src/Web/Features/Contents/Handlers/RemoveCreator.cs +++ b/backend/src/Web/Features/Contents/Handlers/RemoveCreator.cs @@ -40,7 +40,6 @@ public sealed class RemoveCreatorHandler( { var creator = await context .Creators - .Include(c => c.Slugs) .Where(c => c.Id == req.CreatorId) .SingleOrDefaultAsync(cancellationToken: ct); @@ -52,8 +51,6 @@ public sealed class RemoveCreatorHandler( creator.DeletedAt = DateTimeOffset.UtcNow; creator.DeletedBy = User.GetUserId(); - - creator.Slugs.Active = false; await context.SaveChangesAsync(ct); diff --git a/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs b/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs index 7bad1d4..7d83ee9 100644 --- a/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs +++ b/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs @@ -10,8 +10,8 @@ namespace Hutopy.Web.Features.Contents.Handlers; [PublicAPI] public record ReserveSlugRequest { - public string Slug { get; set; } = null!; public required Guid ReservationId { get; set; } + public string Slug { get; set; } = null!; } [PublicAPI] @@ -46,17 +46,26 @@ public sealed class ReserveSlug( try { - await context.Slugs.AddAsync( - new Slugs - { - Id = req.ReservationId, - Active = false, - Name = req.Slug, - ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration, - CreatedBy = User.GetUserId(), - }, + var reservation = await context.Slugs.FirstOrDefaultAsync( + s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(), cancellationToken: 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); diff --git a/frontend/src/views/creators/CreateCreator.vue b/frontend/src/views/creators/CreateCreator.vue index e1484c4..85f6c09 100644 --- a/frontend/src/views/creators/CreateCreator.vue +++ b/frontend/src/views/creators/CreateCreator.vue @@ -19,7 +19,7 @@ const creatorProfileStore = useCreatorProfileStore(); const userProfileStore = useUserProfileStore(); function handleCreatorNameReservationIdChanged($event) { - creatorNameReservationId.value = $event.value + creatorNameReservationId.value = $event } function cancel () { @@ -70,7 +70,7 @@ async function createAccount() {
diff --git a/frontend/src/views/creators/NameEditor.vue b/frontend/src/views/creators/NameEditor.vue index b1db705..366d55c 100644 --- a/frontend/src/views/creators/NameEditor.vue +++ b/frontend/src/views/creators/NameEditor.vue @@ -22,7 +22,7 @@ const isReserved = computed(() => reservationState.value === 'reserved'); const isOperationPending = ref(false); const reservationState = ref(null); -const reservationId = ref(null); +const reservationId = ref(v7()); let timeout = null; const handleInput = () => { @@ -40,21 +40,19 @@ const checkNameAvailability = async () => { } try { - const id = v7(); isOperationPending.value = true; reservationState.value = "loading"; await client.post( `/api/creators/@${encodeURIComponent(name.value)}/reserve`, - {reservationId: id} + {reservationId: reservationId.value} ); reservationState.value = "reserved"; - reservationId.value = id; } catch (error) { reservationState.value = "unavailable"; // Handle API failure case reservationId.value = undefined; } finally { - emits('update:name', name); - emits('update:creatorNameReservationId', reservationId); + emits('update:name', name.value); + emits('update:creatorNameReservationId', reservationId.value); isOperationPending.value = false; } };