Introduces a global Products catalog plus per-family overrides and
private FamilyProducts, exposed via /api/products with a merged
search. Shopping list items and recipe ingredients gain optional
ProductId/FamilyProductId links, and a new ProductStoreSection
table remembers which section a product was last placed in at a
given store so future adds auto-assign the right section.
Frontend gets a reusable ProductTypeahead component, wired into
list-item add and recipe ingredient entry with free-form fallback.
A startup CatalogSeeder loads ~115 curated staples from an embedded
JSON resource via INSERT ... ON CONFLICT DO NOTHING; skipped under
the Testing environment so integration tests keep a clean slate.
No deployed environments yet, so consolidate the entire migration history
into a single Initial migration and tighten the schema accordingly.
- User.Email is now non-nullable (required); the partial unique index
used to tolerate legacy null emails is gone in favor of a plain
unique index.
- Both register paths require an email up front. Invite-path
registrations must match the invited address (server ignores any
mismatched client value); family-code registrations bind whatever the
user supplies but stay unconfirmed (EmailConfirmedAt = null) since
the family code does not prove email ownership.
- Validation order in /register reworked: invite/family-code resolution
runs before duplicate-name/email checks so a consumed token surfaces
a clean "invitation invalid" error instead of getting masked by the
duplicate-email response.
- All 14 prior migrations replaced with a single Initial migration.
- Test fixtures, builders, and unit tests updated to supply emails.
- Login page register form now collects an email field; invite-bound
registrations show the invited address as a read-only input.
Local dev DBs need to be recreated (drop the yeschef-pgdata volume or
the yeschef Postgres database). No production data exists yet.
Users with a confirmed email can now reset a forgotten password via
emailed single-use links.
Backend
- New PasswordResetToken entity (hash-only, 15-minute TTL).
- POST /api/auth/forgot-password always returns 200, never disclosing
whether an email is registered. Internally only emits a reset email
when a user exists with EmailConfirmedAt set, and burns any existing
outstanding tokens for that user before issuing a new one.
- POST /api/auth/reset-password validates the token, rotates the
password hash, and consumes the token. Single-use, expiry-checked.
- Both endpoints are rate-limited (forgot 5/hr, reset 10/15min) per the
same partitioning the login/register endpoints already use.
- Reset email template added; uses AppBaseUrl plumbed in the previous PR.
Frontend
- /forgot-password page (email field, generic confirmation message
regardless of whether the email is registered).
- /reset-password page reads ?token=, validates the new password
client-side, posts to the API, then redirects to /login.
- "Forgot your password?" link added under the login form.
Tests
- 9 new integration tests cover the happy path, single-use enforcement,
expired/unknown tokens, short-password rejection, silent 200 for
unknown email, no email for unconfirmed users, and outstanding-token
invalidation when a fresh request is made.
Family admins can now invite new members by email. The recipient gets a
templated email with a single-use, time-limited join link; clicking it
opens the registration form bound to the invite, and submitting the
form simultaneously consumes the invite and marks the email confirmed.
Self-registration via the shareable family code remains available.
Backend
- New Invite entity (token hash only — raw token never stored), with
per-family uniqueness on the active hash.
- User gains nullable Email and EmailConfirmedAt; partial unique index
so legacy rows with no email do not collide.
- /api/family/invites — admin endpoints to list pending, issue, resend
(rotates the token), and revoke.
- /api/auth/invite/{token} — public lookup returning email + family name
so the registration form can show "you have been invited to X".
- /api/auth/register accepts InviteToken; the invite vouches for the
email, so any client-supplied email field is ignored. Falls back to
FamilyCode when no invite token is present.
- AppUrlOptions / AppBaseUrl plumbed through so emails build absolute
links to the deployed frontend.
Frontend
- /login reads ?invite=<token>, looks it up, and switches the form into
invite-registration mode (pre-binding to the invited email + family).
- /family admin section gains an invite-by-email form, a pending list,
and resend/revoke actions with a confirmation modal.
Tests
- 14 new integration tests covering: admin issue, member-forbidden,
lookup, valid/expired/consumed/unknown-token registration, resend
rotation, revocation, pending-only list filter, and conflict for
inviting an existing member. RecordingEmailSender captures dispatched
messages so tests can assert on the link without standing up SMTP.
Each store gets a StoreSection catalog (Produce, Dairy, etc.). Default
sections are seeded on store creation; admins can rename, reorder, add,
or delete. ShoppingListItem.SectionId is a nullable FK that sets to
null when the section is deleted, so items survive section churn.
The list detail view groups items by section in walk order, with
Uncategorized appended last. Section dropdowns on each row (and the
add-item form) let users assign or reassign on the fly. SignalR
broadcasts include sectionId on adds and a new ItemSectionChanged
event for live re-grouping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Soft-remove items via RemovedAt/RemovedByUserId instead of hard
deleting so the row survives for undo and future history reporting.
DELETE now sets the removal fields; a new POST .../restore clears
them. Active list reads (summary, detail, check toggle) filter to
RemovedAt IS NULL. Frontend surfaces an Undo toast on remove and
handles a new ItemRestored SignalR event.
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>
Foundation for the multi-tenant migration: adds the Family table with a
unique InviteCode, and a startup hook that bootstraps a single default
family from the FamilyCode config when the table is empty. No behavior
change yet — the table exists and is seeded but nothing reads it.
Also fixes the backend test command in CLAUDE.md: dotnet test on the
.NET 10 SDK with MTP rejects the --solution switch and positional
project args, so we now use Push-Location + --project.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (.NET 10 minimal API):
- Vertical slice architecture with feature folders
- Postgres via EF Core with initial migration
- JWT auth with family invite code registration
- REST endpoints for stores, shopping lists, items, recipes
- SignalR hub for real-time list collaboration (per-list groups
and lists-overview group for live list creation/archival/progress)
- Multi-stage Dockerfile
Frontend (SvelteKit + Svelte 5 runes, Tailwind v4):
- Mobile-first PWA with web manifest and service worker
- Bottom-nav layout, login/register, lists overview, list detail,
stores management, recipes (list/create/detail with add-to-list)
- SignalR client with reference-counted connection
- Real-time updates on both lists overview and list detail pages
Infrastructure:
- docker-compose.yml with postgres, backend, frontend services
and Traefik labels for path-based routing (/api, /hubs to backend)
- .env.example with required config
End-to-end tests (Playwright):
- test-e2e.mjs: single-user flow (auth, stores, lists, items, recipes)
- test-e2e-multiuser.mjs: two-user real-time sync coverage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>