Accept a workspace invitation
Java · Spring Boot · intermediate · modification
Adds invitation acceptance: `InviteAcceptController` exposes `POST /invitations/{token}/accept`, and `InviteAcceptanceService` loads the invitation, checks that the signed-in account owns the invited address, then finds the pending member row by email, activates it and links it to the user. Wrong-address and already-accepted cases return 403 and 409. Walked through invite and accept locally.
Team-workspace SaaS on Spring Boot 3.3 (Java 21), Spring Data JPA with Hibernate 6.5 on PostgreSQL 16. `Member`: `Long id`, `Long workspaceId`, `String email` (stored lower-cased), `MemberStatus status` (`INVITED` or `ACTIVE`), `Long userId`, with a unique constraint on `(workspace_id, email)`; people who work with several clients routinely have a member row in each client's workspace under the same address. `Invitation`: `Long id`, unique `String token`, `Long workspaceId`, `String email` (lower-cased at creation), with getters. `Member.activate(userId)` sets the user id and status. `InvitationRepository.findByToken(String)` exists. Spring Security is configured to require authentication for every route outside the public pages, and `AccountPrincipal(long id, String email)` is the existing `@AuthenticationPrincipal` type. Tokens are single-use in practice because accepting leaves the member `ACTIVE`; token expiry is checked by an existing filter before the controller. Transactions are managed by Spring (`@Transactional` on the service).
Requirements
- A signed-in user accepts an invitation with `POST /invitations/{token}/accept`. Invitation creation already exists: it saves an `Invitation` (token, workspace id, lower-cased email) and a `Member` row in that workspace with status `INVITED`. Accepting turns that member row `ACTIVE` and links it to the caller's user id.
- One email address can have a member row in several workspaces (the table is unique on `(workspace_id, email)`, not on `email`). Accepting an invitation must activate the member of the workspace the invitation was issued for, and must not change or depend on the member rows of any other workspace.
- An unknown token is a 404. A caller whose account email differs from the invited address (compared ignoring case) is a 403. A member who is already `ACTIVE` in that workspace gets a 409.
- Account emails are unique and verified, and the caller's email comes from the authenticated principal.
Files touched
- src/main/java/com/acme/workspace/members/MemberRepository.java
- src/main/java/com/acme/workspace/members/InviteAcceptanceService.java
- src/main/java/com/acme/workspace/members/InviteAcceptController.java
--- src/main/java/com/acme/workspace/members/MemberRepository.java
import java.util.List;
+import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByWorkspaceId(Long workspaceId);
+
+ Optional<Member> findByEmail(String email);
}
--- src/main/java/com/acme/workspace/members/InviteAcceptanceService.java
+package com.acme.workspace.members;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.server.ResponseStatusException;
+
+@Service
+public class InviteAcceptanceService {
+
+ private final InvitationRepository invitations;
+ private final MemberRepository members;
+
+ public InviteAcceptanceService(InvitationRepository invitations, MemberRepository members) {
+ this.invitations = invitations;
+ this.members = members;
+ }