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>
This commit is contained in:
2026-04-10 22:22:54 +02:00
parent 0ab1ba0b1b
commit 73af11e84b
2 changed files with 17 additions and 1 deletions

View File

@@ -205,7 +205,9 @@ public class HouseholdService {
HouseholdInvite invite = householdInviteRepository.findByInviteCode(code)
.orElseThrow(() -> new ResourceNotFoundException("Invite not found or invalid"));
if ("used".equals(invite.getStatus()) || invite.getExpiresAt().isBefore(Instant.now())) {
if ("used".equals(invite.getStatus())
|| invite.getInvalidatedAt() != null
|| invite.getExpiresAt().isBefore(Instant.now())) {
throw new ResourceNotFoundException("Invite not found or invalid");
}

View File

@@ -288,6 +288,20 @@ class HouseholdServiceTest {
.isInstanceOf(ResourceNotFoundException.class);
}
@Test
void acceptInviteShouldThrow404WhenInviteIsInvalidated() {
var owner = testUser();
var household = new Household("Smith family", owner);
var invite = new HouseholdInvite(household, "SUPERSEDED", Instant.now().plusSeconds(86400));
invite.setInvalidatedAt(Instant.now()); // superseded by a new invite
when(userAccountRepository.existsByEmailIgnoreCase("tom@example.com")).thenReturn(false);
when(householdInviteRepository.findByInviteCode("SUPERSEDED")).thenReturn(Optional.of(invite));
assertThatThrownBy(() -> householdService.acceptInvite("SUPERSEDED", "Tom", "tom@example.com", "secret123"))
.isInstanceOf(ResourceNotFoundException.class);
}
@Test
void createHouseholdShouldThrowWhenUserNotFound() {
when(userAccountRepository.findByEmailIgnoreCase("unknown@example.com")).thenReturn(Optional.empty());