Commit Graph

54 Commits

Author SHA1 Message Date
0b182a33fd refactor(auth): extract authenticateInSession to AuthService
Remove duplicated private authenticateInSession from AuthController and
HouseholdController. Add a single public implementation on AuthService
with session fixation protection built in. HouseholdController now
injects AuthService and passes role "user" for invite-accepted accounts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:24:58 +02:00
73af11e84b fix(invite): reject invalidated invites in acceptInvite
Same invalidatedAt gap as getInviteInfo: a superseded invite (status
still 'pending', invalidatedAt set) could still be used to create an
account and join the household.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:22:54 +02:00
0ab1ba0b1b fix(invite): reject invalidated invites in getInviteInfo
Superseded invites had invalidatedAt set but status stayed 'pending',
so they passed the validity check and could still be viewed and accepted.
Add invalidatedAt != null guard to getInviteInfo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:22:07 +02:00
44fd398701 fix(invite): saveAndFlush invalidation before INSERT + set invalidated_at on accept
- createInvite: use saveAndFlush when invalidating existing invite so the
  UPDATE is guaranteed to hit the DB before the new INSERT, preventing
  duplicate key violation on uq_household_invite_active
- acceptInvite: also set invalidated_at when marking invite as used, so
  accepted invites are fully removed from the partial unique index and
  cannot block future invite creation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:04:24 +02:00
6aed303627 fix(join): permit /v1/invites/** (not just /*) + match panel color to login
- SecurityConfig: /** covers /v1/invites/{code}/accept (two path segments);
  /* only matched one segment so the accept endpoint was returning 401
- HouseholdIdentityPanel + page: use --green-dark bg (matching BrandPanel
  on login) instead of --green-tint; text updated to white/--green-light

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:00:47 +02:00
92f25e56fc feat(invite): add GET /v1/invites/{code} + rework POST accept as signup+join
- V027 migration: add invited_by FK column on household_invite
- HouseholdInvite entity: add invitedBy field, set on createInvite
- New DTOs: InviteInfoResponse, AcceptInviteRequest
- HouseholdService: add getInviteInfo(), rewrite acceptInvite(code, name, email, password) — creates UserAccount + joins household in one transaction
- HouseholdController: GET /v1/invites/{code} (unauthenticated), POST /v1/invites/{code}/accept creates session after join
- SecurityConfig: permitAll() for /v1/invites/*, sessionFixation().changeSessionId()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 21:24:26 +02:00
9d3be84a0c fix(members): guard against removing the last planner from household
removeMember now checks the planner count before deleting a planner
member. Throws ConflictException("Cannot remove the last planner")
when only one planner remains, matching the spec requirement in S4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 20:30:55 +02:00
eb5699577b feat(backend): make invite share URL base configurable via app.base-url
Replaces hardcoded \"https://yourapp.com\" with a Spring property.

- application.yml: app.base-url defaults to http://localhost:5173
- application-docker.yml: reads APP_BASE_URL env var, same default
- HouseholdService: injects @Value("${app.base-url}") and uses it in
  toInviteResponse() to build shareUrl
- HouseholdServiceTest: sets field via ReflectionTestUtils in @BeforeEach;
  adds test asserting shareUrl starts with configured base URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 20:07:53 +02:00
d1e4b6c49e feat(members): implement DELETE/PATCH member + GET invites backend endpoints
- Add V006 migration: invalidated_at column + partial unique index on household_invite
- Add findByHouseholdIdAndInvalidatedAtIsNull, findByHouseholdIdAndUserId, countByHouseholdIdAndRole
- Add ChangeRoleRequest DTO
- HouseholdService: getActiveInvite, createInvite (regenerate), removeMember, changeMemberRole
- HouseholdController: GET /v1/households/mine/invites, DELETE/PATCH /v1/households/mine/members/{userId}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 18:41:38 +02:00
d901310897 feat(backend): add heroImageUrl and tags to RecipeSummaryResponse
GET /v1/recipes was returning RecipeSummaryResponse with no tags and
only heroImagePreview. The planner frontend needs protein tags to pick
gradient backgrounds for tiles without a hero image.

- Replace JPQL constructor projection with entity query + LEFT JOIN FETCH tags
- Map Recipe entity to RecipeSummaryResponse in service (includes tags + heroImageUrl)
- Drop heroImagePreview in favour of heroImageUrl on the summary DTO

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:03:03 +02:00
16e1539ac0 chore: merge master — adopt SlotResponse.SlotRecipe in SuggestionItem
Resolves conflict by keeping master's refactor: SuggestionItem now reuses
SlotResponse.SlotRecipe instead of the dedicated SuggestionRecipe record,
removing the duplication and adding heroImageUrl to suggestion responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 10:08:38 +02:00
ed769b18a4 fix(recipe): add server-side image size limit and use .matches() for type check
- @Size(max=7_000_000) on heroImageUrl enforces ~5 MB cap at bean validation
- ALLOWED_IMAGE_PATTERN uses .matches() for unambiguous full-string check
- Tests: oversized image → 400, empty ingredients list → 400

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:27:35 +02:00
f11cca534f feat(recipe): compress hero image to 400px preview on save
Adds Thumbnailator-based ImageCompressor that resizes uploaded images
to a 400px-wide JPEG preview stored in hero_image_preview. The recipe
list uses the preview instead of the full image URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:14:35 +02:00
90cff0c4d2 feat(recipe): validate heroImageUrl content type before persisting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:08:45 +02:00
30ba53099c refactor(recipes): drop is_child_friendly column and remove from all layers
V025 migration drops the column. Removed from Recipe entity, RecipeDetailResponse,
RecipeSummaryResponse, RecipeRepository JPQL, RecipeService, and RecipeController.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 08:56:57 +02:00
520dae5adf feat(recipes): add image upload, fix save 500, seed HelloFresh data
- Store hero image as base64 data URI in text column (V023 migration)
- Add file upload UI to RecipeForm with FileReader preview
- Remove isChildFriendly from RecipeCreateRequest (no form field)
- Fix 500 on save: effort values now lowercase, serves/cookTimeMin changed
  from primitive short to nullable Integer to survive omitted fields
- Fix empty categories panel: removed stale tagType=category filter
- Group category tags by type with German headings in recipe form
- Split SuggestionResponse.SuggestionRecipe (no image) from SlotRecipe
- Seed 11 HelloFresh recipes with ingredients, steps and tags (V101)
- Add frontend e2e scaffold, specs and dev yml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 20:23:28 +02:00
0596fddcd3 refactor(planning): extract applyPenalties helper to unify score formula
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
8dfc3df06b fix(planning): hasConflict only when scoreDelta strictly negative
Neutral suggestions (scoreDelta = 0) are not conflicts — they simply
don't improve variety. Changing scoreDelta <= 0 to scoreDelta < 0
lets empty-plan additions and quality-neutral swaps show without a
misleading ⚠ Variationskonflikt warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
ea070b4760 fix(planning): replace existing slot in simulation instead of appending
simulateVarietyScore was adding the candidate recipe on top of the
existing slot for slotDate, keeping the old recipe's tag-repeat penalty
in the score. Now the existing slot is excluded before simulating, so
swapping a recipe for one with better variety correctly shows positive
scoreDelta and hasConflict=false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
9928591b48 refactor(planner): extract computeCurrentScore helper in PlanningService
Eliminates duplicated currentSlots→score pattern that appeared in both
getSuggestions and getVarietyPreview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
8051fcbe22 refactor(planner): extract MAX_VARIETY_SCORE constant in PlanningService
Replaces magic literal 10.0 with a named constant in all four
scoring sites: getSuggestions, getVarietyPreview, scoreFromSimulatedSlots,
and getVarietyScore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
b673a466e9 feat(planner): replace simulatedScore with scoreDelta + hasConflict in SuggestionItem
SuggestionItem now exposes scoreDelta (simulatedScore − currentScore) and
hasConflict (scoreDelta ≤ 0) so the frontend can render badges without
needing to pass currentVarietyScore as a separate prop.

PlanningService.getSuggestions() computes currentScore once per request
and derives scoreDelta + hasConflict per candidate. Sorting is unchanged
(scoreDelta desc = simulatedScore desc since currentScore is constant).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
116e400a91 refactor(planning): extract applyPenalties helper to unify score formula
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:08:49 +02:00
77cdccb26c fix(planning): hasConflict only when scoreDelta strictly negative
Neutral suggestions (scoreDelta = 0) are not conflicts — they simply
don't improve variety. Changing scoreDelta <= 0 to scoreDelta < 0
lets empty-plan additions and quality-neutral swaps show without a
misleading ⚠ Variationskonflikt warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:47:41 +02:00
1611ddabf6 fix(planning): replace existing slot in simulation instead of appending
simulateVarietyScore was adding the candidate recipe on top of the
existing slot for slotDate, keeping the old recipe's tag-repeat penalty
in the score. Now the existing slot is excluded before simulating, so
swapping a recipe for one with better variety correctly shows positive
scoreDelta and hasConflict=false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:31:24 +02:00
0a9e8032cf refactor(planner): extract computeCurrentScore helper in PlanningService
Eliminates duplicated currentSlots→score pattern that appeared in both
getSuggestions and getVarietyPreview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:11:44 +02:00
482597bb6a refactor(planner): extract MAX_VARIETY_SCORE constant in PlanningService
Replaces magic literal 10.0 with a named constant in all four
scoring sites: getSuggestions, getVarietyPreview, scoreFromSimulatedSlots,
and getVarietyScore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:09:08 +02:00
d008a17735 feat(planner): replace simulatedScore with scoreDelta + hasConflict in SuggestionItem
SuggestionItem now exposes scoreDelta (simulatedScore − currentScore) and
hasConflict (scoreDelta ≤ 0) so the frontend can render badges without
needing to pass currentVarietyScore as a separate prop.

PlanningService.getSuggestions() computes currentScore once per request
and derives scoreDelta + hasConflict per candidate. Sorting is unchanged
(scoreDelta desc = simulatedScore desc since currentScore is constant).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 11:33:52 +02:00
ea7113ec53 fix(backend): add role guard to variety-preview and extract shared scoring method
- Add @RequiresHouseholdRole("member") to GET /{planId}/variety-preview endpoint
  to require household membership (was accessible to any authenticated user)
- Extract scoreFromSimulatedSlots() private method eliminating duplicate logic
  between simulateVarietyScore() and the old computeCurrentScore()
- Fix loose variety preview test assertions (isBetween → exact assertEquals)
- Add test verifying negative scoreDelta when candidate is a duplicate recipe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 08:11:45 +02:00
7175b56833 feat(planning): add GET /v1/week-plans/{planId}/variety-preview endpoint
Returns currentScore, projectedScore, and scoreDelta when a recipe
would be added on a given date. Used by C6 desktop day picker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 22:36:03 +02:00
a52b0a9d24 feat(planning): enforce planner role on slot mutation endpoints
PATCH, DELETE, and POST slot endpoints now return 403 Forbidden
when called by a household member.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 22:34:28 +02:00
9d210befa1 fix(security): add @Valid constraints on AddItemRequest to prevent oversized input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:49:06 +02:00
40a6a0e92d fix(security): use generic forbidden message to avoid leaking required role
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:46:24 +02:00
40ee4dad53 refactor(shopping): extract mergeKey helper to eliminate duplicate key construction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:44:44 +02:00
92922533ac feat(shopping): finalize GET /v1/shopping-list endpoint and regenerate OpenAPI types
Renamed endpoint to /v1/shopping-list to avoid Springdoc path conflict.
Added @RequiresHouseholdRole("planner") on generate. Regenerated
frontend OpenAPI schema with all new shopping list endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:49:08 +02:00
16b70bd818 feat(shopping): add GET /v1/shopping-lists endpoint and planner-only guard
New week-based lookup endpoint with optional weekStart param (defaults
to current week). Generate endpoint now enforced with @RequiresHouseholdRole.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:35:31 +02:00
5325f4827e feat(shopping): refactor generateFromPlan to merge strategy
When a shopping list already exists for the week plan, regeneration
now merges: custom items and check states are preserved, existing
generated items are updated, removed recipes' items are deleted,
and new ingredients are added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:33:15 +02:00
c26c2e1973 feat(shopping): add getByWeekStart to ShoppingService
Returns the shopping list for a given week, defaulting to the current
week's Monday when no weekStart is provided. Returns null when no
list exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:30:41 +02:00
93e8bf9e41 feat(shopping): extend response with generatedAt, filteredStaplesCount, RecipeRef
Shopping list response now includes generatedAt timestamp, count of
filtered staples, and recipe names (not just UUIDs) in source references.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:29:07 +02:00
7e254fc280 feat(shopping): add week-based shopping list repository query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:23:43 +02:00
3be9f502c6 feat(auth): add @RequiresHouseholdRole annotation with interceptor
Reusable annotation for planner-only endpoints. Uses a
HandlerInterceptor that resolves the household role from the
authenticated user and throws 403 if the role doesn't match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:22:47 +02:00
2f690eb3cb feat(common): add ForbiddenException with 403 handler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:18:27 +02:00
2253c76287 feat(shopping): add generated_at column to shopping_list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:17:12 +02:00
27e09a77d6 fix(recipes): cast JPQL null params to avoid lower(bytea) error on PostgreSQL
When :search or :effort are null, Hibernate passes untyped bind parameters
that PostgreSQL infers as bytea, causing `lower(bytea) does not exist`.
Explicit CAST(… AS string) tells Hibernate to bind them as varchar.

Also fixes the bcrypt hash in V100 dev seed (was wrong, dev/dev now works).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:10:59 +02:00
93ce1eaeac refactor(auth): add comments, clearContext on logout, explain session auth
- Add comment to SecurityConfig explaining why CSRF is disabled
- Add SecurityContextHolder.clearContext() to logout for clean thread state
- Add Javadoc on authenticateInSession() explaining manual session setup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 18:52:16 +02:00
0aa65214fc fix(auth): resolve broken signup/login flow end-to-end
Three root causes fixed:

1. CSRF blocked all backend POSTs — Spring Security's CSRF filter ran
   before permitAll() authorization, returning 401 for signup and login.
   Disabled CSRF since SvelteKit is the only client (never the browser
   directly) and handles its own CSRF via Origin header checks.

2. Login/signup didn't establish Spring Security authentication — they
   stored email in the HTTP session manually but never set the
   SecurityContext, so Principal in /v1/auth/me was always null and
   hooks.server.ts redirected every authenticated request to /login.
   Fixed with authenticateInSession() helper that sets and persists
   the SecurityContext under SPRING_SECURITY_CONTEXT_KEY. Login also
   now invalidates the old session before creating a new one to prevent
   session fixation.

3. redirect() missing throw in hooks.server.ts, signup action, and
   login action — SvelteKit never saw the redirect, so pages silently
   reloaded with no navigation. Also forward JSESSIONID from backend
   response to browser explicitly, since SvelteKit does not
   auto-forward Set-Cookie for cross-origin server-side fetches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 17:31:29 +02:00
9713412d42 Remove service interfaces — use concrete classes directly
Each domain had a single-implementation interface (e.g. AdminService
interface + AdminServiceImpl). Merged implementation into the service
class and deleted the redundant interfaces per KISS principle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 11:04:41 +02:00
03b96e8584 Remove shopping list draft/publish workflow — lists are always live
Shopping lists no longer go through a draft → published lifecycle.
They are immediately usable upon generation from a week plan.

Removed: status/published_at columns (V021 migration), publish endpoint,
PublishResponse DTO, delete-item guard, and 4 related tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 11:03:54 +02:00
8221a1fd41 Rewrite variety score and suggestions with configurable scoring
- Add VarietyScoreConfig entity, repository, and V020 migration for
  per-household scoring weights and configurable tag types
- Rewrite getVarietyScore: tag-type repeats on consecutive days,
  non-staple ingredient overlaps, cooking log history, plan duplicates
- Rewrite getSuggestions: simulate variety score for each candidate,
  add tag filter (AND, case-insensitive) and configurable topN param
- Update SuggestionResponse to return simulatedScore instead of
  fitReasons/warnings, update VarietyScoreResponse to new shape
- Seed default VarietyScoreConfig on household creation
- Extend test suite across all domains (+270 tests, all passing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 10:33:11 +02:00
9ec703abcd Implement Recipe, Planning, Shopping, Pantry, and Admin domains
Outside-in TDD for all 5 remaining domains (128 tests total):
- Recipe: CRUD, ingredients autocomplete/patch, tags, categories (27 tests)
- Planning: week plans, slots, confirm, suggestions, variety score, cooking logs (24 tests)
- Shopping: generate from plan, publish, check/add/remove items (15 tests)
- Pantry: CRUD with expiry sorting (11 tests)
- Admin: user management, password reset, audit logging (13 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 21:56:51 +02:00