9b2db931ee
Adds FamilyMembership join (UserId, FamilyId, Role) and a non-null FamilyId FK on Store, ShoppingList, ShoppingListItem, Recipe, and RecipeIngredient. FamilyId is denormalized on items/ingredients so the tenant filter is a single column predicate without joins. Store name uniqueness is now scoped per family. JWT issuance stamps a family_id claim; ClaimsPrincipalExtensions exposes GetFamilyId(). Register validates the supplied invite code against Family.InviteCode (replacing the env-var equality check) and writes a FamilyMembership row. OnTokenValidated rejects requests whose user has been removed from the claimed family since login. Every endpoint filters by FamilyId on read and stamps it on write. Cross-family storeId references on list create/update return 400. The SignalR hub verifies list ownership on JoinList and uses a per-family overview group, so cross-tenant fan-out is structurally impossible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
3.7 KiB
C#
83 lines
3.7 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using YesChef.Api.Entities;
|
|
|
|
namespace YesChef.Api.Data;
|
|
|
|
public class YesChefDb(DbContextOptions<YesChefDb> options) : DbContext(options)
|
|
{
|
|
public DbSet<Family> Families => Set<Family>();
|
|
public DbSet<FamilyMembership> FamilyMemberships => Set<FamilyMembership>();
|
|
public DbSet<User> Users => Set<User>();
|
|
public DbSet<Store> Stores => Set<Store>();
|
|
public DbSet<ShoppingList> ShoppingLists => Set<ShoppingList>();
|
|
public DbSet<ShoppingListItem> ShoppingListItems => Set<ShoppingListItem>();
|
|
public DbSet<Recipe> Recipes => Set<Recipe>();
|
|
public DbSet<RecipeIngredient> RecipeIngredients => Set<RecipeIngredient>();
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<Family>(e =>
|
|
{
|
|
e.HasIndex(f => f.InviteCode).IsUnique();
|
|
e.Property(f => f.Name).HasMaxLength(100);
|
|
e.Property(f => f.InviteCode).HasMaxLength(100);
|
|
});
|
|
|
|
modelBuilder.Entity<FamilyMembership>(e =>
|
|
{
|
|
e.HasKey(m => new { m.UserId, m.FamilyId });
|
|
e.HasOne(m => m.User).WithMany().HasForeignKey(m => m.UserId).OnDelete(DeleteBehavior.Cascade);
|
|
e.HasOne(m => m.Family).WithMany().HasForeignKey(m => m.FamilyId).OnDelete(DeleteBehavior.Cascade);
|
|
e.Property(m => m.Role).HasConversion<int>();
|
|
});
|
|
|
|
modelBuilder.Entity<User>(e =>
|
|
{
|
|
e.HasIndex(u => u.Name).IsUnique();
|
|
e.Property(u => u.Name).HasMaxLength(100);
|
|
});
|
|
|
|
modelBuilder.Entity<Store>(e =>
|
|
{
|
|
e.HasOne(s => s.Family).WithMany().HasForeignKey(s => s.FamilyId).OnDelete(DeleteBehavior.Cascade);
|
|
e.HasIndex(s => new { s.FamilyId, s.Name }).IsUnique();
|
|
e.Property(s => s.Name).HasMaxLength(100);
|
|
});
|
|
|
|
modelBuilder.Entity<ShoppingList>(e =>
|
|
{
|
|
e.Property(l => l.Name).HasMaxLength(200);
|
|
e.HasOne(l => l.Family).WithMany().HasForeignKey(l => l.FamilyId).OnDelete(DeleteBehavior.Cascade);
|
|
e.HasOne(l => l.Store).WithMany().HasForeignKey(l => l.StoreId);
|
|
e.HasOne(l => l.CreatedByUser).WithMany().HasForeignKey(l => l.CreatedByUserId);
|
|
e.HasMany(l => l.Items).WithOne(i => i.ShoppingList).HasForeignKey(i => i.ShoppingListId).OnDelete(DeleteBehavior.Cascade);
|
|
e.HasIndex(l => l.FamilyId);
|
|
});
|
|
|
|
modelBuilder.Entity<ShoppingListItem>(e =>
|
|
{
|
|
e.Property(i => i.Name).HasMaxLength(300);
|
|
e.HasOne(i => i.Family).WithMany().HasForeignKey(i => i.FamilyId).OnDelete(DeleteBehavior.Cascade);
|
|
e.HasOne(i => i.CheckedByUser).WithMany().HasForeignKey(i => i.CheckedByUserId).OnDelete(DeleteBehavior.SetNull);
|
|
e.HasOne(i => i.Recipe).WithMany().HasForeignKey(i => i.RecipeId).OnDelete(DeleteBehavior.SetNull);
|
|
e.HasIndex(i => i.FamilyId);
|
|
});
|
|
|
|
modelBuilder.Entity<Recipe>(e =>
|
|
{
|
|
e.Property(r => r.Title).HasMaxLength(300);
|
|
e.HasOne(r => r.Family).WithMany().HasForeignKey(r => r.FamilyId).OnDelete(DeleteBehavior.Cascade);
|
|
e.HasOne(r => r.CreatedByUser).WithMany().HasForeignKey(r => r.CreatedByUserId);
|
|
e.HasMany(r => r.Ingredients).WithOne(i => i.Recipe).HasForeignKey(i => i.RecipeId).OnDelete(DeleteBehavior.Cascade);
|
|
e.HasIndex(r => r.FamilyId);
|
|
});
|
|
|
|
modelBuilder.Entity<RecipeIngredient>(e =>
|
|
{
|
|
e.Property(i => i.Name).HasMaxLength(200);
|
|
e.Property(i => i.Quantity).HasMaxLength(50);
|
|
e.HasOne(i => i.Family).WithMany().HasForeignKey(i => i.FamilyId).OnDelete(DeleteBehavior.Cascade);
|
|
});
|
|
}
|
|
}
|