Scope all data access by FamilyId for multi-tenant isolation

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>
This commit is contained in:
Josh Rogers
2026-05-07 23:05:23 -05:00
parent 7c1cfd62e6
commit 9b2db931ee
25 changed files with 1057 additions and 90 deletions
@@ -3,6 +3,8 @@ namespace YesChef.Api.Entities;
public class Recipe
{
public int Id { get; set; }
public int FamilyId { get; set; }
public Family Family { get; set; } = null!;
public required string Title { get; set; }
public string? Description { get; set; }
public string? Instructions { get; set; }