Commit Graph

18 Commits

Author SHA1 Message Date
Josh Rogers 09bec105f6 Collapse migrations, require email at registration
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.
2026-05-08 22:58:27 -05:00
Josh Rogers af085cfb90 Add password reset flow
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.
2026-05-08 22:47:33 -05:00
Josh Rogers d9ffe18b21 Add email-based invites and email confirmation in one flow
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.
2026-05-08 22:42:55 -05:00
Josh Rogers a1635218a8 Add SMTP infrastructure + auth rate limiting
Foundation for the upcoming email-based invite and password-reset flows.

- IEmailSender abstraction with SmtpEmailSender (MailKit 4.16) and a
  LoggingEmailSender fallback used automatically when SMTP is unconfigured
  so local dev works without a real SMTP server.
- Fixed-window rate limits keyed by remote IP: 10 / 15 min on /login,
  5 / hour on /register. Returns 429 with Retry-After. Bypassed in the
  Testing environment so the existing integration suite is unaffected.
- New env vars (SMTP_*, APP_BASE_URL) plumbed through docker-compose
  and documented in .env.example.
2026-05-08 22:35:57 -05:00
Josh Rogers 86603b4f4a Backlog: email-based admin invites + email confirmation in one flow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:06:03 -05:00
Josh Rogers fa1d4b4f62 Add per-store sections to group list items by walk order
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>
2026-05-08 22:05:57 -05:00
Josh Rogers de5c18f3e6 Add per-family invite codes and admin roles
First member into a family becomes Admin; subsequent members default to
Member. JWTs carry a family_role claim that is refreshed from the DB on
each request so promotions and demotions take effect immediately.

New /api/family endpoints let admins view the roster, regenerate the
invite code, change roles, and remove members. Last-admin and
self-removal guards prevent locking the family out of management.

The /family page exposes the same actions in the UI; the bottom nav now
links there. Members see the roster but not the invite code.

Existing deployments get a one-time backfill at startup: any family with
members but no admin gets its earliest-joined member promoted.
2026-05-08 21:29:15 -05:00
Josh Rogers d4db819e72 Polish Store endpoints: 409 conflicts and confirm-before-delete
Backend pre-checks duplicate names on POST/PUT and returns 409
Conflict (replacing the previous 500 from the unique constraint).
DELETE-with-active-lists also returns 409 instead of 400 for
semantic accuracy.

Frontend addStore and saveEdit now surface API errors via toast
instead of failing silently. Delete is now gated by a confirmation
modal so accidental clicks no longer destroy data.
2026-05-08 21:07:57 -05:00
Josh Rogers 7fcae09afb Distinguish picked-up from removed shopping list items
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.
2026-05-08 20:07:41 -05:00
Josh Rogers 9b2db931ee 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>
2026-05-07 23:05:23 -05:00
Josh Rogers 7c1cfd62e6 Introduce Family entity and bootstrap default family on startup
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>
2026-05-07 23:00:00 -05:00
Josh Rogers 6f71f8c2d6 Add Playwright MCP server config and ignore its scratch dir
Checks in `.mcp.json` so the Playwright MCP server is available to
all contributors out of the box, and ignores `.playwright-mcp/` where
the server stashes per-session snapshots and console logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:09:20 -05:00
Josh Rogers 88c24b03ca Add reusable toast notification system
Introduces a runes-based toast module (`$lib/toast.svelte.ts`) with
success/info/warning/error variants, auto-dismiss, optional action
button, plus a `Toaster` viewport mounted in the root layout.
Migrates the lone `alert(e.message)` call site (stores delete) to
`toast.error()`. Backlog updated to remove the now-completed
foundational item and rewrite dependent items to reference the
shipped API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:08:52 -05:00
Josh Rogers 6780fb366e Add BACKLOG.md and ideas.md from product exploration session
Captures the design direction from a multi-feature exploratory session:
- Multi-tenant Family epic (decided)
- Product + unit catalogs with family-scoped overrides and free-form additions
- Per-store sections / departments with grouped list UI
- Structured quantities + UoM (required on recipes, optional on lists with sensible defaults)
- Pick-up vs remove distinction for shopping list items
- Add-recipe-to-list flow
- Stores polish: types, confirm-before-delete, duplicate-name bug, toast component
- Foundational items (auth lifecycle, conflict resolution, offline PWA, backup/restore, onboarding, accessibility)

ideas.md holds longer-term explorations (recipe URL import, product images,
item notes, meal planning, observability).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:32:56 -05:00
Josh Rogers cde619e730 Add Vitest unit tests and Playwright e2e suite for frontend
Replaces the ad-hoc test-e2e.mjs scripts with a proper test stack:
Vitest covers $lib (api, auth, signalr) with mocked fetch/SignalR/navigation,
and @playwright/test covers auth, stores, lists, recipes, and SignalR realtime
sync between two browser contexts. Tests use uniqueName() for every entity
since the backend has no per-test reset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:17:55 -05:00
Josh Rogers 76e8de9484 Add TUnit-based unit and integration tests for backend
Set up YesChef.Api.UnitTests and YesChef.Api.IntegrationTests projects
running on TUnit + Microsoft.Testing.Platform. Integration tests use a
single Postgres 17 Testcontainer per session and clone a migrated
template database per test (`CREATE DATABASE … TEMPLATE …`) so tests
remain fully isolated and run in parallel without replaying migrations
each time.

Test-author DX is built around fluent entity builders, a TestDataFactory
for common scenarios, and a two-level base hierarchy
(IntegrationTest / AuthenticatedIntegrationTest) whose `[Before(Test)]`
hooks stand up the per-test database, app factory, default user, and
authenticated HttpClient — leaving each test body focused on the action
under test.

Adds src/backend/global.json to opt `dotnet test` into MTP mode on the
.NET 10 SDK, and updates CLAUDE.md with how to run the tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 20:56:29 -05:00
Josh Rogers 7ca2dc46d9 Add CLAUDE.md with repo orientation for Claude Code
Captures stack shape, common commands (with Windows-friendly --prefix/--project
forms), backend feature-folder layout, JWT + SignalR auth flow, and required
.env keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 19:40:12 -05:00
Josh Rogers 48d30df07b Initial commit: YesChef family shopping list and recipe app
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>
2026-05-06 19:32:39 -05:00