diff --git a/src/backend/YesChef.Api.IntegrationTests/Builders/UserBuilder.cs b/src/backend/YesChef.Api.IntegrationTests/Builders/UserBuilder.cs index 5c2dd42..938a0b9 100644 --- a/src/backend/YesChef.Api.IntegrationTests/Builders/UserBuilder.cs +++ b/src/backend/YesChef.Api.IntegrationTests/Builders/UserBuilder.cs @@ -8,15 +8,22 @@ public sealed class UserBuilder { private string _name = $"user-{Guid.NewGuid():N}"[..16]; private string _password = "correct-horse-battery-staple"; + private string? _email; public UserBuilder Named(string name) { _name = name; return this; } public UserBuilder WithPassword(string password) { _password = password; return this; } + public UserBuilder WithEmail(string email) { _email = email; return this; } public string PlaintextPassword => _password; public User Build() { - var user = new User { Name = _name, PasswordHash = "" }; + var user = new User + { + Name = _name, + PasswordHash = "", + Email = _email ?? $"{_name}@example.test", + }; user.PasswordHash = new PasswordHasher().HashPassword(user, _password); return user; } diff --git a/src/backend/YesChef.Api.IntegrationTests/Features/AuthEndpointsTests.cs b/src/backend/YesChef.Api.IntegrationTests/Features/AuthEndpointsTests.cs index 2399572..0eb0109 100644 --- a/src/backend/YesChef.Api.IntegrationTests/Features/AuthEndpointsTests.cs +++ b/src/backend/YesChef.Api.IntegrationTests/Features/AuthEndpointsTests.cs @@ -11,7 +11,7 @@ public class AuthEndpointsTests : IntegrationTest public async Task Register_creates_user_and_returns_token() { var response = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("alice", "hunter2", YesChefAppFactory.FamilyCode)); + new AuthEndpoints.RegisterRequest("alice", "hunter2", "alice@example.com", YesChefAppFactory.FamilyCode)); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); var body = await response.Content.ReadFromJsonAsync(); @@ -24,7 +24,7 @@ public class AuthEndpointsTests : IntegrationTest public async Task Register_rejects_wrong_family_code() { var response = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("bob", "pw", "wrong-code")); + new AuthEndpoints.RegisterRequest("bob", "pw", "bob@example.com", "wrong-code")); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest); } @@ -35,7 +35,7 @@ public class AuthEndpointsTests : IntegrationTest await Data.RegisterAsync("carol"); var response = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("carol", "another", YesChefAppFactory.FamilyCode)); + new AuthEndpoints.RegisterRequest("carol", "another", "carol-2@example.com", YesChefAppFactory.FamilyCode)); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Conflict); } diff --git a/src/backend/YesChef.Api.IntegrationTests/Features/AuthRateLimitTests.cs b/src/backend/YesChef.Api.IntegrationTests/Features/AuthRateLimitTests.cs index 16a3361..2dd612a 100644 --- a/src/backend/YesChef.Api.IntegrationTests/Features/AuthRateLimitTests.cs +++ b/src/backend/YesChef.Api.IntegrationTests/Features/AuthRateLimitTests.cs @@ -56,7 +56,7 @@ public class AuthRateLimitTests : IntegrationTest { lastResponse?.Dispose(); lastResponse = await client.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest($"rate-{i}", "pw-1234", YesChefAppFactory.FamilyCode)); + new AuthEndpoints.RegisterRequest($"rate-{i}", "pw-1234", $"rate-{i}@example.com", YesChefAppFactory.FamilyCode)); } await Assert.That(lastResponse!.StatusCode).IsEqualTo(HttpStatusCode.TooManyRequests); diff --git a/src/backend/YesChef.Api.IntegrationTests/Features/InviteEndpointsTests.cs b/src/backend/YesChef.Api.IntegrationTests/Features/InviteEndpointsTests.cs index 49658e2..2b287e0 100644 --- a/src/backend/YesChef.Api.IntegrationTests/Features/InviteEndpointsTests.cs +++ b/src/backend/YesChef.Api.IntegrationTests/Features/InviteEndpointsTests.cs @@ -66,7 +66,7 @@ public class InviteEndpointsTests : AuthenticatedIntegrationTest var token = await IssueInviteAndExtractTokenAsync("carol@example.com"); var register = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("carol", "pw-1234", null, token)); + new AuthEndpoints.RegisterRequest("carol", "pw-1234", "carol@example.com", null, token)); await Assert.That(register.StatusCode).IsEqualTo(HttpStatusCode.OK); var user = await UseDbAsync(db => db.Users.SingleAsync(u => u.Name == "carol")); @@ -84,7 +84,7 @@ public class InviteEndpointsTests : AuthenticatedIntegrationTest var token = await IssueInviteAndExtractTokenAsync("dave@example.com"); await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("dave", "pw-1234", null, token)); + new AuthEndpoints.RegisterRequest("dave", "pw-1234", "dave@example.com", null, token)); var (familyId, role) = await UseDbAsync(db => (from u in db.Users @@ -104,11 +104,11 @@ public class InviteEndpointsTests : AuthenticatedIntegrationTest { var token = await IssueInviteAndExtractTokenAsync("eve@example.com"); var first = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("eve", "pw-1234", null, token)); + new AuthEndpoints.RegisterRequest("eve", "pw-1234", "eve@example.com", null, token)); await Assert.That(first.StatusCode).IsEqualTo(HttpStatusCode.OK); var second = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("eve-2", "pw-1234", null, token)); + new AuthEndpoints.RegisterRequest("eve-2", "pw-1234", "eve@example.com", null, token)); await Assert.That(second.StatusCode).IsEqualTo(HttpStatusCode.BadRequest); } @@ -117,7 +117,7 @@ public class InviteEndpointsTests : AuthenticatedIntegrationTest public async Task Register_with_unknown_token_is_rejected() { var response = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("fran", "pw-1234", null, "this-token-does-not-exist")); + new AuthEndpoints.RegisterRequest("fran", "pw-1234", "fran@example.com", null, "this-token-does-not-exist")); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest); } @@ -135,7 +135,7 @@ public class InviteEndpointsTests : AuthenticatedIntegrationTest }); var response = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("greg", "pw-1234", null, token)); + new AuthEndpoints.RegisterRequest("greg", "pw-1234", "greg@example.com", null, token)); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest); } @@ -144,7 +144,7 @@ public class InviteEndpointsTests : AuthenticatedIntegrationTest public async Task Register_without_invite_or_family_code_is_rejected() { var response = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest("hank", "pw-1234")); + new AuthEndpoints.RegisterRequest("hank", "pw-1234", "hank@example.com")); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest); } @@ -206,18 +206,13 @@ public class InviteEndpointsTests : AuthenticatedIntegrationTest [Test] public async Task Invite_for_existing_member_returns_conflict() { - // Make the admin's email match the invite target so the same-email - // member check trips. (Direct DB set — there's no API to attach an - // email to an existing user yet.) - await UseDbAsync(async db => - { - var me = await db.Users.SingleAsync(u => u.Id == User.Id); - me.Email = "already@example.com"; - await db.SaveChangesAsync(); - }); + // The bootstrap admin (User) has an email assigned at registration + // time by TestDataFactory. Inviting that exact address must trip the + // same-email check. + var existing = await UseDbAsync(db => db.Users.Where(u => u.Id == User.Id).Select(u => u.Email).SingleAsync()); var response = await Client.PostAsJsonAsync("/api/family/invites", - new InviteEndpoints.CreateInviteRequest("already@example.com")); + new InviteEndpoints.CreateInviteRequest(existing)); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Conflict); } diff --git a/src/backend/YesChef.Api.IntegrationTests/Features/PasswordResetTests.cs b/src/backend/YesChef.Api.IntegrationTests/Features/PasswordResetTests.cs index ced89e5..396412a 100644 --- a/src/backend/YesChef.Api.IntegrationTests/Features/PasswordResetTests.cs +++ b/src/backend/YesChef.Api.IntegrationTests/Features/PasswordResetTests.cs @@ -46,9 +46,9 @@ public class PasswordResetTests : AuthenticatedIntegrationTest [Test] public async Task Forgot_password_does_not_email_unconfirmed_user() { - // The default authed user (registered via family code) has no email, - // so they're ineligible. Attach an unconfirmed email directly to - // verify the EmailConfirmedAt requirement fires. + // The default authed user has an email (TestDataFactory always sets + // one) but registered via the family-code path, which does not set + // EmailConfirmedAt. Replace the address so we can target it explicitly. await UseDbAsync(async db => { var u = await db.Users.SingleAsync(x => x.Id == User.Id); @@ -185,7 +185,7 @@ public class PasswordResetTests : AuthenticatedIntegrationTest var inviteToken = ExtractInviteToken(); var register = await AnonymousClient.PostAsJsonAsync("/api/auth/register", - new AuthEndpoints.RegisterRequest(name, "pw-1234", null, inviteToken)); + new AuthEndpoints.RegisterRequest(name, "pw-1234", email, null, inviteToken)); register.EnsureSuccessStatusCode(); return (name, email); } diff --git a/src/backend/YesChef.Api.IntegrationTests/Infrastructure/TestDataFactory.cs b/src/backend/YesChef.Api.IntegrationTests/Infrastructure/TestDataFactory.cs index 0e4d38c..1792144 100644 --- a/src/backend/YesChef.Api.IntegrationTests/Infrastructure/TestDataFactory.cs +++ b/src/backend/YesChef.Api.IntegrationTests/Infrastructure/TestDataFactory.cs @@ -49,7 +49,8 @@ public sealed class TestDataFactory(IntegrationTest test) public async Task RegisterAsync(string? name = null, string password = "correct-horse-battery-staple") { name ??= $"user-{Guid.NewGuid():N}"[..16]; - var register = new AuthEndpoints.RegisterRequest(name, password, YesChefAppFactory.FamilyCode); + var email = $"{name}@example.test"; + var register = new AuthEndpoints.RegisterRequest(name, password, email, YesChefAppFactory.FamilyCode); var response = await test.AnonymousClient.PostAsJsonAsync("/api/auth/register", register); response.EnsureSuccessStatusCode(); var auth = await response.Content.ReadFromJsonAsync(); diff --git a/src/backend/YesChef.Api.UnitTests/Auth/JwtTokenServiceTests.cs b/src/backend/YesChef.Api.UnitTests/Auth/JwtTokenServiceTests.cs index 3fd2e1e..242756c 100644 --- a/src/backend/YesChef.Api.UnitTests/Auth/JwtTokenServiceTests.cs +++ b/src/backend/YesChef.Api.UnitTests/Auth/JwtTokenServiceTests.cs @@ -27,7 +27,7 @@ public class JwtTokenServiceTests public async Task GenerateToken_includes_user_id_name_and_family_claims() { var service = BuildService(); - var user = new User { Id = 42, Name = "alice", PasswordHash = "x" }; + var user = new User { Id = 42, Name = "alice", PasswordHash = "x", Email = "u@example.com" }; var jwt = Decode(service.GenerateToken(user, familyId: 7, role: FamilyRole.Admin)); @@ -41,7 +41,7 @@ public class JwtTokenServiceTests public async Task GenerateToken_expires_in_about_30_days() { var service = BuildService(); - var user = new User { Id = 1, Name = "bob", PasswordHash = "x" }; + var user = new User { Id = 1, Name = "bob", PasswordHash = "x", Email = "u@example.com" }; var jwt = Decode(service.GenerateToken(user, familyId: 1, role: FamilyRole.Member)); @@ -54,7 +54,7 @@ public class JwtTokenServiceTests public async Task GenerateToken_signs_with_hs256_using_configured_secret() { var service = BuildService(); - var user = new User { Id = 7, Name = "carol", PasswordHash = "x" }; + var user = new User { Id = 7, Name = "carol", PasswordHash = "x", Email = "u@example.com" }; var token = service.GenerateToken(user, familyId: 1, role: FamilyRole.Member); @@ -79,7 +79,7 @@ public class JwtTokenServiceTests public async Task GenerateToken_with_different_secret_fails_validation() { var service = BuildService(); - var token = service.GenerateToken(new User { Id = 1, Name = "x", PasswordHash = "x" }, familyId: 1, role: FamilyRole.Member); + var token = service.GenerateToken(new User { Id = 1, Name = "x", PasswordHash = "x", Email = "u@example.com" }, familyId: 1, role: FamilyRole.Member); var validator = new JwtSecurityTokenHandler(); var parameters = new TokenValidationParameters diff --git a/src/backend/YesChef.Api/Auth/AuthEndpoints.cs b/src/backend/YesChef.Api/Auth/AuthEndpoints.cs index a6ef8e7..613ea46 100644 --- a/src/backend/YesChef.Api/Auth/AuthEndpoints.cs +++ b/src/backend/YesChef.Api/Auth/AuthEndpoints.cs @@ -13,9 +13,10 @@ public static class AuthEndpoints /// /// Registration request. Either or /// must be supplied — invite tokens take precedence - /// when both are present. + /// when both are present. is always required; on the + /// invite path it must match the invite's email (the invite vouches for it). /// - public record RegisterRequest(string Name, string Password, string? FamilyCode = null, string? InviteToken = null); + public record RegisterRequest(string Name, string Password, string Email, string? FamilyCode = null, string? InviteToken = null); public record LoginRequest(string Name, string Password); public record AuthResponse(string Token, string Name, string Role); public record InviteLookupResponse(string Email, string FamilyName); @@ -28,9 +29,14 @@ public static class AuthEndpoints group.MapPost("/register", async (RegisterRequest request, YesChefDb db, JwtTokenService jwt) => { - if (await db.Users.AnyAsync(u => u.Name == request.Name)) - return Results.Conflict(new { error = "Name already taken." }); + if (string.IsNullOrWhiteSpace(request.Email)) + return Results.BadRequest(new { error = "An email address is required." }); + var normalizedEmail = request.Email.Trim().ToLowerInvariant(); + + // Validate the invite/family-code first so consumed/expired tokens + // give a clear "invitation invalid" error rather than getting masked + // by the duplicate-email check that would otherwise fire next. Family family; Invite? invite = null; @@ -41,6 +47,11 @@ public static class AuthEndpoints if (invite is null || invite.ConsumedAt is not null || invite.ExpiresAt < DateTime.UtcNow) return Results.BadRequest(new { error = "Invitation is invalid or has expired." }); + // The invite vouches for a specific address; reject anything else + // so a tampered client can't bind a different email to the family. + if (!string.Equals(invite.Email, normalizedEmail, StringComparison.OrdinalIgnoreCase)) + return Results.BadRequest(new { error = "Email does not match the invitation." }); + family = await db.Families.FirstAsync(f => f.Id == invite.FamilyId); } else if (!string.IsNullOrWhiteSpace(request.FamilyCode)) @@ -55,16 +66,21 @@ public static class AuthEndpoints return Results.BadRequest(new { error = "An invite link or family code is required." }); } - var user = new User { Name = request.Name, PasswordHash = "" }; - user.PasswordHash = hasher.HashPassword(user, request.Password); + if (await db.Users.AnyAsync(u => u.Name == request.Name)) + return Results.Conflict(new { error = "Name already taken." }); + if (await db.Users.AnyAsync(u => u.Email == normalizedEmail)) + return Results.Conflict(new { error = "Email already registered." }); - // The invite vouches for the email; trust it over anything the client - // might attach (no client-supplied email field today, but be explicit). - if (invite is not null) + var user = new User { - user.Email = invite.Email; - user.EmailConfirmedAt = DateTime.UtcNow; - } + Name = request.Name, + PasswordHash = "", + Email = normalizedEmail, + // Invite path proves email ownership; family-code path does not, + // so those users are unconfirmed until a future verification flow. + EmailConfirmedAt = invite is not null ? DateTime.UtcNow : null, + }; + user.PasswordHash = hasher.HashPassword(user, request.Password); db.Users.Add(user); await db.SaveChangesAsync(); diff --git a/src/backend/YesChef.Api/Data/YesChefDb.cs b/src/backend/YesChef.Api/Data/YesChefDb.cs index c6f4284..77801d5 100644 --- a/src/backend/YesChef.Api/Data/YesChefDb.cs +++ b/src/backend/YesChef.Api/Data/YesChefDb.cs @@ -39,9 +39,7 @@ public class YesChefDb(DbContextOptions options) : DbContext(options) e.HasIndex(u => u.Name).IsUnique(); e.Property(u => u.Name).HasMaxLength(100); e.Property(u => u.Email).HasMaxLength(254); - // Partial unique index: only enforced where Email is set so legacy - // rows (and any future user without an email) don't collide on null. - e.HasIndex(u => u.Email).IsUnique().HasFilter("\"Email\" IS NOT NULL"); + e.HasIndex(u => u.Email).IsUnique(); }); modelBuilder.Entity(e => diff --git a/src/backend/YesChef.Api/Entities/User.cs b/src/backend/YesChef.Api/Entities/User.cs index 2317a22..0b775a1 100644 --- a/src/backend/YesChef.Api/Entities/User.cs +++ b/src/backend/YesChef.Api/Entities/User.cs @@ -5,7 +5,7 @@ public class User public int Id { get; set; } public required string Name { get; set; } public required string PasswordHash { get; set; } - public string? Email { get; set; } + public required string Email { get; set; } public DateTime? EmailConfirmedAt { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; } diff --git a/src/backend/YesChef.Api/Migrations/20260506041045_InitialCreate.Designer.cs b/src/backend/YesChef.Api/Migrations/20260506041045_InitialCreate.Designer.cs deleted file mode 100644 index fd158d9..0000000 --- a/src/backend/YesChef.Api/Migrations/20260506041045_InitialCreate.Designer.cs +++ /dev/null @@ -1,311 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YesChef.Api.Data; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - [DbContext(typeof(YesChefDb))] - [Migration("20260506041045_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Instructions") - .HasColumnType("text"); - - b.Property("Servings") - .HasColumnType("integer"); - - b.Property("SourceUrl") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.ToTable("Recipes"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Quantity") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("RecipeId"); - - b.ToTable("RecipeIngredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("IsArchived") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("StoreId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("StoreId"); - - b.ToTable("ShoppingLists"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CheckedByUserId") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsChecked") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("ShoppingListId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CheckedByUserId"); - - b.HasIndex("RecipeId"); - - b.HasIndex("ShoppingListId"); - - b.ToTable("ShoppingListItems"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Stores"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany("Ingredients") - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Store", "Store") - .WithMany() - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Store"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.HasOne("YesChef.Api.Entities.User", "CheckedByUser") - .WithMany() - .HasForeignKey("CheckedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany() - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.ShoppingList", "ShoppingList") - .WithMany("Items") - .HasForeignKey("ShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CheckedByUser"); - - b.Navigation("Recipe"); - - b.Navigation("ShoppingList"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Navigation("Ingredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Navigation("Items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260506041045_InitialCreate.cs b/src/backend/YesChef.Api/Migrations/20260506041045_InitialCreate.cs deleted file mode 100644 index c56ee12..0000000 --- a/src/backend/YesChef.Api/Migrations/20260506041045_InitialCreate.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Stores", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - SortOrder = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Stores", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Recipes", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Title = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), - Description = table.Column(type: "text", nullable: true), - Instructions = table.Column(type: "text", nullable: true), - Servings = table.Column(type: "integer", nullable: true), - SourceUrl = table.Column(type: "text", nullable: true), - CreatedByUserId = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Recipes", x => x.Id); - table.ForeignKey( - name: "FK_Recipes_Users_CreatedByUserId", - column: x => x.CreatedByUserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ShoppingLists", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - StoreId = table.Column(type: "integer", nullable: false), - IsArchived = table.Column(type: "boolean", nullable: false), - CreatedByUserId = table.Column(type: "integer", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ShoppingLists", x => x.Id); - table.ForeignKey( - name: "FK_ShoppingLists_Stores_StoreId", - column: x => x.StoreId, - principalTable: "Stores", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ShoppingLists_Users_CreatedByUserId", - column: x => x.CreatedByUserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "RecipeIngredients", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - RecipeId = table.Column(type: "integer", nullable: false), - Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Quantity = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - SortOrder = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_RecipeIngredients", x => x.Id); - table.ForeignKey( - name: "FK_RecipeIngredients_Recipes_RecipeId", - column: x => x.RecipeId, - principalTable: "Recipes", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ShoppingListItems", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ShoppingListId = table.Column(type: "integer", nullable: false), - Name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), - IsChecked = table.Column(type: "boolean", nullable: false), - CheckedByUserId = table.Column(type: "integer", nullable: true), - SortOrder = table.Column(type: "integer", nullable: false), - RecipeId = table.Column(type: "integer", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ShoppingListItems", x => x.Id); - table.ForeignKey( - name: "FK_ShoppingListItems_Recipes_RecipeId", - column: x => x.RecipeId, - principalTable: "Recipes", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - table.ForeignKey( - name: "FK_ShoppingListItems_ShoppingLists_ShoppingListId", - column: x => x.ShoppingListId, - principalTable: "ShoppingLists", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ShoppingListItems_Users_CheckedByUserId", - column: x => x.CheckedByUserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - }); - - migrationBuilder.CreateIndex( - name: "IX_RecipeIngredients_RecipeId", - table: "RecipeIngredients", - column: "RecipeId"); - - migrationBuilder.CreateIndex( - name: "IX_Recipes_CreatedByUserId", - table: "Recipes", - column: "CreatedByUserId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingListItems_CheckedByUserId", - table: "ShoppingListItems", - column: "CheckedByUserId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingListItems_RecipeId", - table: "ShoppingListItems", - column: "RecipeId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingListItems_ShoppingListId", - table: "ShoppingListItems", - column: "ShoppingListId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingLists_CreatedByUserId", - table: "ShoppingLists", - column: "CreatedByUserId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingLists_StoreId", - table: "ShoppingLists", - column: "StoreId"); - - migrationBuilder.CreateIndex( - name: "IX_Stores_Name", - table: "Stores", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Users_Name", - table: "Users", - column: "Name", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "RecipeIngredients"); - - migrationBuilder.DropTable( - name: "ShoppingListItems"); - - migrationBuilder.DropTable( - name: "Recipes"); - - migrationBuilder.DropTable( - name: "ShoppingLists"); - - migrationBuilder.DropTable( - name: "Stores"); - - migrationBuilder.DropTable( - name: "Users"); - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260508033526_AddFamily.Designer.cs b/src/backend/YesChef.Api/Migrations/20260508033526_AddFamily.Designer.cs deleted file mode 100644 index 9558ccc..0000000 --- a/src/backend/YesChef.Api/Migrations/20260508033526_AddFamily.Designer.cs +++ /dev/null @@ -1,340 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YesChef.Api.Data; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - [DbContext(typeof(YesChefDb))] - [Migration("20260508033526_AddFamily")] - partial class AddFamily - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YesChef.Api.Entities.Family", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("InviteCode") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.HasIndex("InviteCode") - .IsUnique(); - - b.ToTable("Families"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Instructions") - .HasColumnType("text"); - - b.Property("Servings") - .HasColumnType("integer"); - - b.Property("SourceUrl") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.ToTable("Recipes"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Quantity") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("RecipeId"); - - b.ToTable("RecipeIngredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("IsArchived") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("StoreId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("StoreId"); - - b.ToTable("ShoppingLists"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CheckedByUserId") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsChecked") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("ShoppingListId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CheckedByUserId"); - - b.HasIndex("RecipeId"); - - b.HasIndex("ShoppingListId"); - - b.ToTable("ShoppingListItems"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Stores"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany("Ingredients") - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Store", "Store") - .WithMany() - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Store"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.HasOne("YesChef.Api.Entities.User", "CheckedByUser") - .WithMany() - .HasForeignKey("CheckedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany() - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.ShoppingList", "ShoppingList") - .WithMany("Items") - .HasForeignKey("ShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CheckedByUser"); - - b.Navigation("Recipe"); - - b.Navigation("ShoppingList"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Navigation("Ingredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Navigation("Items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260508033526_AddFamily.cs b/src/backend/YesChef.Api/Migrations/20260508033526_AddFamily.cs deleted file mode 100644 index dfeb606..0000000 --- a/src/backend/YesChef.Api/Migrations/20260508033526_AddFamily.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - /// - public partial class AddFamily : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Families", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - InviteCode = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Families", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Families_InviteCode", - table: "Families", - column: "InviteCode", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Families"); - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260508034856_AddFamilyScoping.Designer.cs b/src/backend/YesChef.Api/Migrations/20260508034856_AddFamilyScoping.Designer.cs deleted file mode 100644 index 4d937d9..0000000 --- a/src/backend/YesChef.Api/Migrations/20260508034856_AddFamilyScoping.Designer.cs +++ /dev/null @@ -1,446 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YesChef.Api.Data; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - [DbContext(typeof(YesChefDb))] - [Migration("20260508034856_AddFamilyScoping")] - partial class AddFamilyScoping - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YesChef.Api.Entities.Family", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("InviteCode") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.HasIndex("InviteCode") - .IsUnique(); - - b.ToTable("Families"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => - { - b.Property("UserId") - .HasColumnType("integer"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Role") - .HasColumnType("integer"); - - b.HasKey("UserId", "FamilyId"); - - b.HasIndex("FamilyId"); - - b.ToTable("FamilyMemberships"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Instructions") - .HasColumnType("text"); - - b.Property("Servings") - .HasColumnType("integer"); - - b.Property("SourceUrl") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("FamilyId"); - - b.ToTable("Recipes"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Quantity") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId"); - - b.HasIndex("RecipeId"); - - b.ToTable("RecipeIngredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IsArchived") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("StoreId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("FamilyId"); - - b.HasIndex("StoreId"); - - b.ToTable("ShoppingLists"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CheckedByUserId") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IsChecked") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("ShoppingListId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CheckedByUserId"); - - b.HasIndex("FamilyId"); - - b.HasIndex("RecipeId"); - - b.HasIndex("ShoppingListId"); - - b.ToTable("ShoppingListItems"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId", "Name") - .IsUnique(); - - b.ToTable("Stores"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Family"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany("Ingredients") - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Store", "Store") - .WithMany() - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Family"); - - b.Navigation("Store"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.HasOne("YesChef.Api.Entities.User", "CheckedByUser") - .WithMany() - .HasForeignKey("CheckedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany() - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.ShoppingList", "ShoppingList") - .WithMany("Items") - .HasForeignKey("ShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CheckedByUser"); - - b.Navigation("Family"); - - b.Navigation("Recipe"); - - b.Navigation("ShoppingList"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Navigation("Ingredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Navigation("Items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260508034856_AddFamilyScoping.cs b/src/backend/YesChef.Api/Migrations/20260508034856_AddFamilyScoping.cs deleted file mode 100644 index e268366..0000000 --- a/src/backend/YesChef.Api/Migrations/20260508034856_AddFamilyScoping.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - /// - public partial class AddFamilyScoping : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_Stores_Name", - table: "Stores"); - - migrationBuilder.AddColumn( - name: "FamilyId", - table: "Stores", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "FamilyId", - table: "ShoppingLists", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "FamilyId", - table: "ShoppingListItems", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "FamilyId", - table: "Recipes", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "FamilyId", - table: "RecipeIngredients", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.CreateTable( - name: "FamilyMemberships", - columns: table => new - { - UserId = table.Column(type: "integer", nullable: false), - FamilyId = table.Column(type: "integer", nullable: false), - Role = table.Column(type: "integer", nullable: false), - JoinedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_FamilyMemberships", x => new { x.UserId, x.FamilyId }); - table.ForeignKey( - name: "FK_FamilyMemberships_Families_FamilyId", - column: x => x.FamilyId, - principalTable: "Families", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_FamilyMemberships_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Stores_FamilyId_Name", - table: "Stores", - columns: new[] { "FamilyId", "Name" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingLists_FamilyId", - table: "ShoppingLists", - column: "FamilyId"); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingListItems_FamilyId", - table: "ShoppingListItems", - column: "FamilyId"); - - migrationBuilder.CreateIndex( - name: "IX_Recipes_FamilyId", - table: "Recipes", - column: "FamilyId"); - - migrationBuilder.CreateIndex( - name: "IX_RecipeIngredients_FamilyId", - table: "RecipeIngredients", - column: "FamilyId"); - - migrationBuilder.CreateIndex( - name: "IX_FamilyMemberships_FamilyId", - table: "FamilyMemberships", - column: "FamilyId"); - - migrationBuilder.AddForeignKey( - name: "FK_RecipeIngredients_Families_FamilyId", - table: "RecipeIngredients", - column: "FamilyId", - principalTable: "Families", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_Recipes_Families_FamilyId", - table: "Recipes", - column: "FamilyId", - principalTable: "Families", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_ShoppingListItems_Families_FamilyId", - table: "ShoppingListItems", - column: "FamilyId", - principalTable: "Families", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_ShoppingLists_Families_FamilyId", - table: "ShoppingLists", - column: "FamilyId", - principalTable: "Families", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - - migrationBuilder.AddForeignKey( - name: "FK_Stores_Families_FamilyId", - table: "Stores", - column: "FamilyId", - principalTable: "Families", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_RecipeIngredients_Families_FamilyId", - table: "RecipeIngredients"); - - migrationBuilder.DropForeignKey( - name: "FK_Recipes_Families_FamilyId", - table: "Recipes"); - - migrationBuilder.DropForeignKey( - name: "FK_ShoppingListItems_Families_FamilyId", - table: "ShoppingListItems"); - - migrationBuilder.DropForeignKey( - name: "FK_ShoppingLists_Families_FamilyId", - table: "ShoppingLists"); - - migrationBuilder.DropForeignKey( - name: "FK_Stores_Families_FamilyId", - table: "Stores"); - - migrationBuilder.DropTable( - name: "FamilyMemberships"); - - migrationBuilder.DropIndex( - name: "IX_Stores_FamilyId_Name", - table: "Stores"); - - migrationBuilder.DropIndex( - name: "IX_ShoppingLists_FamilyId", - table: "ShoppingLists"); - - migrationBuilder.DropIndex( - name: "IX_ShoppingListItems_FamilyId", - table: "ShoppingListItems"); - - migrationBuilder.DropIndex( - name: "IX_Recipes_FamilyId", - table: "Recipes"); - - migrationBuilder.DropIndex( - name: "IX_RecipeIngredients_FamilyId", - table: "RecipeIngredients"); - - migrationBuilder.DropColumn( - name: "FamilyId", - table: "Stores"); - - migrationBuilder.DropColumn( - name: "FamilyId", - table: "ShoppingLists"); - - migrationBuilder.DropColumn( - name: "FamilyId", - table: "ShoppingListItems"); - - migrationBuilder.DropColumn( - name: "FamilyId", - table: "Recipes"); - - migrationBuilder.DropColumn( - name: "FamilyId", - table: "RecipeIngredients"); - - migrationBuilder.CreateIndex( - name: "IX_Stores_Name", - table: "Stores", - column: "Name", - unique: true); - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260508221005_AddShoppingListItemSoftRemove.Designer.cs b/src/backend/YesChef.Api/Migrations/20260508221005_AddShoppingListItemSoftRemove.Designer.cs deleted file mode 100644 index db36d7d..0000000 --- a/src/backend/YesChef.Api/Migrations/20260508221005_AddShoppingListItemSoftRemove.Designer.cs +++ /dev/null @@ -1,461 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YesChef.Api.Data; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - [DbContext(typeof(YesChefDb))] - [Migration("20260508221005_AddShoppingListItemSoftRemove")] - partial class AddShoppingListItemSoftRemove - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YesChef.Api.Entities.Family", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("InviteCode") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.HasIndex("InviteCode") - .IsUnique(); - - b.ToTable("Families"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => - { - b.Property("UserId") - .HasColumnType("integer"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Role") - .HasColumnType("integer"); - - b.HasKey("UserId", "FamilyId"); - - b.HasIndex("FamilyId"); - - b.ToTable("FamilyMemberships"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Instructions") - .HasColumnType("text"); - - b.Property("Servings") - .HasColumnType("integer"); - - b.Property("SourceUrl") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("FamilyId"); - - b.ToTable("Recipes"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Quantity") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId"); - - b.HasIndex("RecipeId"); - - b.ToTable("RecipeIngredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IsArchived") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("StoreId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("FamilyId"); - - b.HasIndex("StoreId"); - - b.ToTable("ShoppingLists"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CheckedByUserId") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IsChecked") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("RemovedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("RemovedByUserId") - .HasColumnType("integer"); - - b.Property("ShoppingListId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CheckedByUserId"); - - b.HasIndex("FamilyId"); - - b.HasIndex("RecipeId"); - - b.HasIndex("RemovedByUserId"); - - b.HasIndex("ShoppingListId"); - - b.ToTable("ShoppingListItems"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId", "Name") - .IsUnique(); - - b.ToTable("Stores"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Family"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany("Ingredients") - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Store", "Store") - .WithMany() - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Family"); - - b.Navigation("Store"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.HasOne("YesChef.Api.Entities.User", "CheckedByUser") - .WithMany() - .HasForeignKey("CheckedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany() - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.User", "RemovedByUser") - .WithMany() - .HasForeignKey("RemovedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.ShoppingList", "ShoppingList") - .WithMany("Items") - .HasForeignKey("ShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CheckedByUser"); - - b.Navigation("Family"); - - b.Navigation("Recipe"); - - b.Navigation("RemovedByUser"); - - b.Navigation("ShoppingList"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Navigation("Ingredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Navigation("Items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260508221005_AddShoppingListItemSoftRemove.cs b/src/backend/YesChef.Api/Migrations/20260508221005_AddShoppingListItemSoftRemove.cs deleted file mode 100644 index 4a44748..0000000 --- a/src/backend/YesChef.Api/Migrations/20260508221005_AddShoppingListItemSoftRemove.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - /// - public partial class AddShoppingListItemSoftRemove : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "RemovedAt", - table: "ShoppingListItems", - type: "timestamp with time zone", - nullable: true); - - migrationBuilder.AddColumn( - name: "RemovedByUserId", - table: "ShoppingListItems", - type: "integer", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingListItems_RemovedByUserId", - table: "ShoppingListItems", - column: "RemovedByUserId"); - - migrationBuilder.AddForeignKey( - name: "FK_ShoppingListItems_Users_RemovedByUserId", - table: "ShoppingListItems", - column: "RemovedByUserId", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_ShoppingListItems_Users_RemovedByUserId", - table: "ShoppingListItems"); - - migrationBuilder.DropIndex( - name: "IX_ShoppingListItems_RemovedByUserId", - table: "ShoppingListItems"); - - migrationBuilder.DropColumn( - name: "RemovedAt", - table: "ShoppingListItems"); - - migrationBuilder.DropColumn( - name: "RemovedByUserId", - table: "ShoppingListItems"); - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260509025211_AddStoreSections.Designer.cs b/src/backend/YesChef.Api/Migrations/20260509025211_AddStoreSections.Designer.cs deleted file mode 100644 index 11a5de8..0000000 --- a/src/backend/YesChef.Api/Migrations/20260509025211_AddStoreSections.Designer.cs +++ /dev/null @@ -1,524 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YesChef.Api.Data; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - [DbContext(typeof(YesChefDb))] - [Migration("20260509025211_AddStoreSections")] - partial class AddStoreSections - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YesChef.Api.Entities.Family", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("InviteCode") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.HasIndex("InviteCode") - .IsUnique(); - - b.ToTable("Families"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => - { - b.Property("UserId") - .HasColumnType("integer"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Role") - .HasColumnType("integer"); - - b.HasKey("UserId", "FamilyId"); - - b.HasIndex("FamilyId"); - - b.ToTable("FamilyMemberships"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Instructions") - .HasColumnType("text"); - - b.Property("Servings") - .HasColumnType("integer"); - - b.Property("SourceUrl") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("FamilyId"); - - b.ToTable("Recipes"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Quantity") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId"); - - b.HasIndex("RecipeId"); - - b.ToTable("RecipeIngredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IsArchived") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("StoreId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("FamilyId"); - - b.HasIndex("StoreId"); - - b.ToTable("ShoppingLists"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CheckedByUserId") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IsChecked") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("RemovedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("RemovedByUserId") - .HasColumnType("integer"); - - b.Property("SectionId") - .HasColumnType("integer"); - - b.Property("ShoppingListId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CheckedByUserId"); - - b.HasIndex("FamilyId"); - - b.HasIndex("RecipeId"); - - b.HasIndex("RemovedByUserId"); - - b.HasIndex("SectionId"); - - b.HasIndex("ShoppingListId"); - - b.ToTable("ShoppingListItems"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId", "Name") - .IsUnique(); - - b.ToTable("Stores"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.StoreSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId"); - - b.HasIndex("StoreId", "Name") - .IsUnique(); - - b.ToTable("StoreSections"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Family"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany("Ingredients") - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Store", "Store") - .WithMany() - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Family"); - - b.Navigation("Store"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.HasOne("YesChef.Api.Entities.User", "CheckedByUser") - .WithMany() - .HasForeignKey("CheckedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany() - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.User", "RemovedByUser") - .WithMany() - .HasForeignKey("RemovedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.StoreSection", "Section") - .WithMany() - .HasForeignKey("SectionId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.ShoppingList", "ShoppingList") - .WithMany("Items") - .HasForeignKey("ShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CheckedByUser"); - - b.Navigation("Family"); - - b.Navigation("Recipe"); - - b.Navigation("RemovedByUser"); - - b.Navigation("Section"); - - b.Navigation("ShoppingList"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.StoreSection", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Store", "Store") - .WithMany() - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("Store"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Navigation("Ingredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Navigation("Items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260509025211_AddStoreSections.cs b/src/backend/YesChef.Api/Migrations/20260509025211_AddStoreSections.cs deleted file mode 100644 index e6b9a8e..0000000 --- a/src/backend/YesChef.Api/Migrations/20260509025211_AddStoreSections.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - /// - public partial class AddStoreSections : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "SectionId", - table: "ShoppingListItems", - type: "integer", - nullable: true); - - migrationBuilder.CreateTable( - name: "StoreSections", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - FamilyId = table.Column(type: "integer", nullable: false), - StoreId = table.Column(type: "integer", nullable: false), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - SortOrder = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_StoreSections", x => x.Id); - table.ForeignKey( - name: "FK_StoreSections_Families_FamilyId", - column: x => x.FamilyId, - principalTable: "Families", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_StoreSections_Stores_StoreId", - column: x => x.StoreId, - principalTable: "Stores", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ShoppingListItems_SectionId", - table: "ShoppingListItems", - column: "SectionId"); - - migrationBuilder.CreateIndex( - name: "IX_StoreSections_FamilyId", - table: "StoreSections", - column: "FamilyId"); - - migrationBuilder.CreateIndex( - name: "IX_StoreSections_StoreId_Name", - table: "StoreSections", - columns: new[] { "StoreId", "Name" }, - unique: true); - - migrationBuilder.AddForeignKey( - name: "FK_ShoppingListItems_StoreSections_SectionId", - table: "ShoppingListItems", - column: "SectionId", - principalTable: "StoreSections", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_ShoppingListItems_StoreSections_SectionId", - table: "ShoppingListItems"); - - migrationBuilder.DropTable( - name: "StoreSections"); - - migrationBuilder.DropIndex( - name: "IX_ShoppingListItems_SectionId", - table: "ShoppingListItems"); - - migrationBuilder.DropColumn( - name: "SectionId", - table: "ShoppingListItems"); - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260509033654_AddEmailAndInvites.Designer.cs b/src/backend/YesChef.Api/Migrations/20260509033654_AddEmailAndInvites.Designer.cs deleted file mode 100644 index 1993200..0000000 --- a/src/backend/YesChef.Api/Migrations/20260509033654_AddEmailAndInvites.Designer.cs +++ /dev/null @@ -1,611 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using YesChef.Api.Data; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - [DbContext(typeof(YesChefDb))] - [Migration("20260509033654_AddEmailAndInvites")] - partial class AddEmailAndInvites - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.7") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("YesChef.Api.Entities.Family", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("InviteCode") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.HasIndex("InviteCode") - .IsUnique(); - - b.ToTable("Families"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => - { - b.Property("UserId") - .HasColumnType("integer"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("JoinedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Role") - .HasColumnType("integer"); - - b.HasKey("UserId", "FamilyId"); - - b.HasIndex("FamilyId"); - - b.ToTable("FamilyMemberships"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Invite", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("ConsumedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("ConsumedByUserId") - .HasColumnType("integer"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(254) - .HasColumnType("character varying(254)"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IssuedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IssuedByUserId") - .HasColumnType("integer"); - - b.Property("TokenHash") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("character varying(64)"); - - b.HasKey("Id"); - - b.HasIndex("ConsumedByUserId"); - - b.HasIndex("IssuedByUserId"); - - b.HasIndex("TokenHash") - .IsUnique(); - - b.HasIndex("FamilyId", "ConsumedAt"); - - b.ToTable("Invites"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Instructions") - .HasColumnType("text"); - - b.Property("Servings") - .HasColumnType("integer"); - - b.Property("SourceUrl") - .HasColumnType("text"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("FamilyId"); - - b.ToTable("Recipes"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Quantity") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId"); - - b.HasIndex("RecipeId"); - - b.ToTable("RecipeIngredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CreatedByUserId") - .HasColumnType("integer"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IsArchived") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("StoreId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("CreatedByUserId"); - - b.HasIndex("FamilyId"); - - b.HasIndex("StoreId"); - - b.ToTable("ShoppingLists"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CheckedByUserId") - .HasColumnType("integer"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("IsChecked") - .HasColumnType("boolean"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(300) - .HasColumnType("character varying(300)"); - - b.Property("RecipeId") - .HasColumnType("integer"); - - b.Property("RemovedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("RemovedByUserId") - .HasColumnType("integer"); - - b.Property("SectionId") - .HasColumnType("integer"); - - b.Property("ShoppingListId") - .HasColumnType("integer"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CheckedByUserId"); - - b.HasIndex("FamilyId"); - - b.HasIndex("RecipeId"); - - b.HasIndex("RemovedByUserId"); - - b.HasIndex("SectionId"); - - b.HasIndex("ShoppingListId"); - - b.ToTable("ShoppingListItems"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId", "Name") - .IsUnique(); - - b.ToTable("Stores"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.StoreSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("FamilyId") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SortOrder") - .HasColumnType("integer"); - - b.Property("StoreId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("FamilyId"); - - b.HasIndex("StoreId", "Name") - .IsUnique(); - - b.ToTable("StoreSections"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .HasMaxLength(254) - .HasColumnType("character varying(254)"); - - b.Property("EmailConfirmedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique() - .HasFilter("\"Email\" IS NOT NULL"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.FamilyMembership", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Invite", b => - { - b.HasOne("YesChef.Api.Entities.User", "ConsumedByUser") - .WithMany() - .HasForeignKey("ConsumedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.User", "IssuedByUser") - .WithMany() - .HasForeignKey("IssuedByUserId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("ConsumedByUser"); - - b.Navigation("Family"); - - b.Navigation("IssuedByUser"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Family"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.RecipeIngredient", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany("Ingredients") - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("Recipe"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.HasOne("YesChef.Api.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Store", "Store") - .WithMany() - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("Family"); - - b.Navigation("Store"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingListItem", b => - { - b.HasOne("YesChef.Api.Entities.User", "CheckedByUser") - .WithMany() - .HasForeignKey("CheckedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Recipe", "Recipe") - .WithMany() - .HasForeignKey("RecipeId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.User", "RemovedByUser") - .WithMany() - .HasForeignKey("RemovedByUserId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.StoreSection", "Section") - .WithMany() - .HasForeignKey("SectionId") - .OnDelete(DeleteBehavior.SetNull); - - b.HasOne("YesChef.Api.Entities.ShoppingList", "ShoppingList") - .WithMany("Items") - .HasForeignKey("ShoppingListId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CheckedByUser"); - - b.Navigation("Family"); - - b.Navigation("Recipe"); - - b.Navigation("RemovedByUser"); - - b.Navigation("Section"); - - b.Navigation("ShoppingList"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Store", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.StoreSection", b => - { - b.HasOne("YesChef.Api.Entities.Family", "Family") - .WithMany() - .HasForeignKey("FamilyId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("YesChef.Api.Entities.Store", "Store") - .WithMany() - .HasForeignKey("StoreId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Family"); - - b.Navigation("Store"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.Recipe", b => - { - b.Navigation("Ingredients"); - }); - - modelBuilder.Entity("YesChef.Api.Entities.ShoppingList", b => - { - b.Navigation("Items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260509033654_AddEmailAndInvites.cs b/src/backend/YesChef.Api/Migrations/20260509033654_AddEmailAndInvites.cs deleted file mode 100644 index 39d1257..0000000 --- a/src/backend/YesChef.Api/Migrations/20260509033654_AddEmailAndInvites.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - /// - public partial class AddEmailAndInvites : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Email", - table: "Users", - type: "character varying(254)", - maxLength: 254, - nullable: true); - - migrationBuilder.AddColumn( - name: "EmailConfirmedAt", - table: "Users", - type: "timestamp with time zone", - nullable: true); - - migrationBuilder.CreateTable( - name: "Invites", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - FamilyId = table.Column(type: "integer", nullable: false), - Email = table.Column(type: "character varying(254)", maxLength: 254, nullable: false), - TokenHash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - IssuedByUserId = table.Column(type: "integer", nullable: false), - IssuedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), - ConsumedAt = table.Column(type: "timestamp with time zone", nullable: true), - ConsumedByUserId = table.Column(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Invites", x => x.Id); - table.ForeignKey( - name: "FK_Invites_Families_FamilyId", - column: x => x.FamilyId, - principalTable: "Families", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Invites_Users_ConsumedByUserId", - column: x => x.ConsumedByUserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - table.ForeignKey( - name: "FK_Invites_Users_IssuedByUserId", - column: x => x.IssuedByUserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateIndex( - name: "IX_Users_Email", - table: "Users", - column: "Email", - unique: true, - filter: "\"Email\" IS NOT NULL"); - - migrationBuilder.CreateIndex( - name: "IX_Invites_ConsumedByUserId", - table: "Invites", - column: "ConsumedByUserId"); - - migrationBuilder.CreateIndex( - name: "IX_Invites_FamilyId_ConsumedAt", - table: "Invites", - columns: new[] { "FamilyId", "ConsumedAt" }); - - migrationBuilder.CreateIndex( - name: "IX_Invites_IssuedByUserId", - table: "Invites", - column: "IssuedByUserId"); - - migrationBuilder.CreateIndex( - name: "IX_Invites_TokenHash", - table: "Invites", - column: "TokenHash", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Invites"); - - migrationBuilder.DropIndex( - name: "IX_Users_Email", - table: "Users"); - - migrationBuilder.DropColumn( - name: "Email", - table: "Users"); - - migrationBuilder.DropColumn( - name: "EmailConfirmedAt", - table: "Users"); - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260509034418_AddPasswordResetTokens.cs b/src/backend/YesChef.Api/Migrations/20260509034418_AddPasswordResetTokens.cs deleted file mode 100644 index 31113db..0000000 --- a/src/backend/YesChef.Api/Migrations/20260509034418_AddPasswordResetTokens.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace YesChef.Api.Migrations -{ - /// - public partial class AddPasswordResetTokens : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "PasswordResetTokens", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - UserId = table.Column(type: "integer", nullable: false), - TokenHash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - IssuedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), - ConsumedAt = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_PasswordResetTokens", x => x.Id); - table.ForeignKey( - name: "FK_PasswordResetTokens_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_PasswordResetTokens_TokenHash", - table: "PasswordResetTokens", - column: "TokenHash", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_PasswordResetTokens_UserId_ConsumedAt", - table: "PasswordResetTokens", - columns: new[] { "UserId", "ConsumedAt" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "PasswordResetTokens"); - } - } -} diff --git a/src/backend/YesChef.Api/Migrations/20260509034418_AddPasswordResetTokens.Designer.cs b/src/backend/YesChef.Api/Migrations/20260509035449_Initial.Designer.cs similarity index 99% rename from src/backend/YesChef.Api/Migrations/20260509034418_AddPasswordResetTokens.Designer.cs rename to src/backend/YesChef.Api/Migrations/20260509035449_Initial.Designer.cs index 0ed380c..cea1c8d 100644 --- a/src/backend/YesChef.Api/Migrations/20260509034418_AddPasswordResetTokens.Designer.cs +++ b/src/backend/YesChef.Api/Migrations/20260509035449_Initial.Designer.cs @@ -12,8 +12,8 @@ using YesChef.Api.Data; namespace YesChef.Api.Migrations { [DbContext(typeof(YesChefDb))] - [Migration("20260509034418_AddPasswordResetTokens")] - partial class AddPasswordResetTokens + [Migration("20260509035449_Initial")] + partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -417,6 +417,7 @@ namespace YesChef.Api.Migrations .HasColumnType("timestamp with time zone"); b.Property("Email") + .IsRequired() .HasMaxLength(254) .HasColumnType("character varying(254)"); @@ -435,8 +436,7 @@ namespace YesChef.Api.Migrations b.HasKey("Id"); b.HasIndex("Email") - .IsUnique() - .HasFilter("\"Email\" IS NOT NULL"); + .IsUnique(); b.HasIndex("Name") .IsUnique(); diff --git a/src/backend/YesChef.Api/Migrations/20260509035449_Initial.cs b/src/backend/YesChef.Api/Migrations/20260509035449_Initial.cs new file mode 100644 index 0000000..60b5177 --- /dev/null +++ b/src/backend/YesChef.Api/Migrations/20260509035449_Initial.cs @@ -0,0 +1,517 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace YesChef.Api.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Families", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + InviteCode = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Families", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + Email = table.Column(type: "character varying(254)", maxLength: 254, nullable: false), + EmailConfirmedAt = table.Column(type: "timestamp with time zone", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Stores", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FamilyId = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Stores", x => x.Id); + table.ForeignKey( + name: "FK_Stores_Families_FamilyId", + column: x => x.FamilyId, + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FamilyMemberships", + columns: table => new + { + UserId = table.Column(type: "integer", nullable: false), + FamilyId = table.Column(type: "integer", nullable: false), + Role = table.Column(type: "integer", nullable: false), + JoinedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FamilyMemberships", x => new { x.UserId, x.FamilyId }); + table.ForeignKey( + name: "FK_FamilyMemberships_Families_FamilyId", + column: x => x.FamilyId, + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_FamilyMemberships_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Invites", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FamilyId = table.Column(type: "integer", nullable: false), + Email = table.Column(type: "character varying(254)", maxLength: 254, nullable: false), + TokenHash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + IssuedByUserId = table.Column(type: "integer", nullable: false), + IssuedAt = table.Column(type: "timestamp with time zone", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + ConsumedAt = table.Column(type: "timestamp with time zone", nullable: true), + ConsumedByUserId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Invites", x => x.Id); + table.ForeignKey( + name: "FK_Invites_Families_FamilyId", + column: x => x.FamilyId, + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Invites_Users_ConsumedByUserId", + column: x => x.ConsumedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_Invites_Users_IssuedByUserId", + column: x => x.IssuedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "PasswordResetTokens", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "integer", nullable: false), + TokenHash = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + IssuedAt = table.Column(type: "timestamp with time zone", nullable: false), + ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), + ConsumedAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PasswordResetTokens", x => x.Id); + table.ForeignKey( + name: "FK_PasswordResetTokens_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Recipes", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FamilyId = table.Column(type: "integer", nullable: false), + Title = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + Description = table.Column(type: "text", nullable: true), + Instructions = table.Column(type: "text", nullable: true), + Servings = table.Column(type: "integer", nullable: true), + SourceUrl = table.Column(type: "text", nullable: true), + CreatedByUserId = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Recipes", x => x.Id); + table.ForeignKey( + name: "FK_Recipes_Families_FamilyId", + column: x => x.FamilyId, + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Recipes_Users_CreatedByUserId", + column: x => x.CreatedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingLists", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FamilyId = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + StoreId = table.Column(type: "integer", nullable: false), + IsArchived = table.Column(type: "boolean", nullable: false), + CreatedByUserId = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingLists", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingLists_Families_FamilyId", + column: x => x.FamilyId, + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingLists_Stores_StoreId", + column: x => x.StoreId, + principalTable: "Stores", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingLists_Users_CreatedByUserId", + column: x => x.CreatedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "StoreSections", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FamilyId = table.Column(type: "integer", nullable: false), + StoreId = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + SortOrder = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StoreSections", x => x.Id); + table.ForeignKey( + name: "FK_StoreSections_Families_FamilyId", + column: x => x.FamilyId, + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_StoreSections_Stores_StoreId", + column: x => x.StoreId, + principalTable: "Stores", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RecipeIngredients", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FamilyId = table.Column(type: "integer", nullable: false), + RecipeId = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Quantity = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + SortOrder = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RecipeIngredients", x => x.Id); + table.ForeignKey( + name: "FK_RecipeIngredients_Families_FamilyId", + column: x => x.FamilyId, + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RecipeIngredients_Recipes_RecipeId", + column: x => x.RecipeId, + principalTable: "Recipes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ShoppingListItems", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + FamilyId = table.Column(type: "integer", nullable: false), + ShoppingListId = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), + IsChecked = table.Column(type: "boolean", nullable: false), + CheckedByUserId = table.Column(type: "integer", nullable: true), + SortOrder = table.Column(type: "integer", nullable: false), + RecipeId = table.Column(type: "integer", nullable: true), + SectionId = table.Column(type: "integer", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + RemovedAt = table.Column(type: "timestamp with time zone", nullable: true), + RemovedByUserId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ShoppingListItems", x => x.Id); + table.ForeignKey( + name: "FK_ShoppingListItems_Families_FamilyId", + column: x => x.FamilyId, + principalTable: "Families", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingListItems_Recipes_RecipeId", + column: x => x.RecipeId, + principalTable: "Recipes", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ShoppingListItems_ShoppingLists_ShoppingListId", + column: x => x.ShoppingListId, + principalTable: "ShoppingLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ShoppingListItems_StoreSections_SectionId", + column: x => x.SectionId, + principalTable: "StoreSections", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ShoppingListItems_Users_CheckedByUserId", + column: x => x.CheckedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_ShoppingListItems_Users_RemovedByUserId", + column: x => x.RemovedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_Families_InviteCode", + table: "Families", + column: "InviteCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FamilyMemberships_FamilyId", + table: "FamilyMemberships", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_ConsumedByUserId", + table: "Invites", + column: "ConsumedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_FamilyId_ConsumedAt", + table: "Invites", + columns: new[] { "FamilyId", "ConsumedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_Invites_IssuedByUserId", + table: "Invites", + column: "IssuedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_TokenHash", + table: "Invites", + column: "TokenHash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PasswordResetTokens_TokenHash", + table: "PasswordResetTokens", + column: "TokenHash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PasswordResetTokens_UserId_ConsumedAt", + table: "PasswordResetTokens", + columns: new[] { "UserId", "ConsumedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_RecipeIngredients_FamilyId", + table: "RecipeIngredients", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_RecipeIngredients_RecipeId", + table: "RecipeIngredients", + column: "RecipeId"); + + migrationBuilder.CreateIndex( + name: "IX_Recipes_CreatedByUserId", + table: "Recipes", + column: "CreatedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Recipes_FamilyId", + table: "Recipes", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingListItems_CheckedByUserId", + table: "ShoppingListItems", + column: "CheckedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingListItems_FamilyId", + table: "ShoppingListItems", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingListItems_RecipeId", + table: "ShoppingListItems", + column: "RecipeId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingListItems_RemovedByUserId", + table: "ShoppingListItems", + column: "RemovedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingListItems_SectionId", + table: "ShoppingListItems", + column: "SectionId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingListItems_ShoppingListId", + table: "ShoppingListItems", + column: "ShoppingListId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingLists_CreatedByUserId", + table: "ShoppingLists", + column: "CreatedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingLists_FamilyId", + table: "ShoppingLists", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_ShoppingLists_StoreId", + table: "ShoppingLists", + column: "StoreId"); + + migrationBuilder.CreateIndex( + name: "IX_Stores_FamilyId_Name", + table: "Stores", + columns: new[] { "FamilyId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_StoreSections_FamilyId", + table: "StoreSections", + column: "FamilyId"); + + migrationBuilder.CreateIndex( + name: "IX_StoreSections_StoreId_Name", + table: "StoreSections", + columns: new[] { "StoreId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Name", + table: "Users", + column: "Name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FamilyMemberships"); + + migrationBuilder.DropTable( + name: "Invites"); + + migrationBuilder.DropTable( + name: "PasswordResetTokens"); + + migrationBuilder.DropTable( + name: "RecipeIngredients"); + + migrationBuilder.DropTable( + name: "ShoppingListItems"); + + migrationBuilder.DropTable( + name: "Recipes"); + + migrationBuilder.DropTable( + name: "ShoppingLists"); + + migrationBuilder.DropTable( + name: "StoreSections"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Stores"); + + migrationBuilder.DropTable( + name: "Families"); + } + } +} diff --git a/src/backend/YesChef.Api/Migrations/YesChefDbModelSnapshot.cs b/src/backend/YesChef.Api/Migrations/YesChefDbModelSnapshot.cs index fa85c2d..311d8a3 100644 --- a/src/backend/YesChef.Api/Migrations/YesChefDbModelSnapshot.cs +++ b/src/backend/YesChef.Api/Migrations/YesChefDbModelSnapshot.cs @@ -414,6 +414,7 @@ namespace YesChef.Api.Migrations .HasColumnType("timestamp with time zone"); b.Property("Email") + .IsRequired() .HasMaxLength(254) .HasColumnType("character varying(254)"); @@ -432,8 +433,7 @@ namespace YesChef.Api.Migrations b.HasKey("Id"); b.HasIndex("Email") - .IsUnique() - .HasFilter("\"Email\" IS NOT NULL"); + .IsUnique(); b.HasIndex("Name") .IsUnique(); diff --git a/src/frontend/src/routes/login/+page.svelte b/src/frontend/src/routes/login/+page.svelte index c22894d..7e2da8d 100644 --- a/src/frontend/src/routes/login/+page.svelte +++ b/src/frontend/src/routes/login/+page.svelte @@ -12,6 +12,7 @@ let mode = $state<'login' | 'register'>('login'); let name = $state(''); let password = $state(''); + let email = $state(''); let familyCode = $state(''); let error = $state(''); let loading = $state(false); @@ -29,6 +30,7 @@ inviteLoading = true; try { invite = await api(`/api/auth/invite/${encodeURIComponent(token)}`); + email = invite.email; } catch { // Surface a clear message rather than the generic API error — the // recipient probably arrived from a stale or revoked email link. @@ -49,9 +51,9 @@ if (mode === 'login') { body = { name, password }; } else if (inviteToken) { - body = { name, password, inviteToken }; + body = { name, password, email, inviteToken }; } else { - body = { name, password, familyCode }; + body = { name, password, email, familyCode }; } const res = await api<{ token: string }>(endpoint, { @@ -111,16 +113,28 @@ /> - {#if mode === 'register' && !inviteToken} + {#if mode === 'register'}
+ {#if !inviteToken} +
+ +
+ {/if} {/if} {#if error}