--- title: 'Team Invitation Acceptance Flow' type: 'feature' created: '2026-03-27' status: 'done' baseline_commit: '8f39bd9' context: ['_bmad-output/project-context.md'] --- # Team Invitation Acceptance Flow ## Intent **Problem:** Clicking the team invitation email link lands on a standard registration page that ignores the invitation token. New users register without being attached to the inviting workspace; existing users have no acceptance path at all. **Approach:** Create a neutral entry route `/team/invitation/{token}/accept` that validates the token and routes to the correct flow: auto-accept for authenticated users, redirect to login (with message) for existing accounts, or redirect to register (with pre-filled email) for new users. Modify `CreateNewUser` to process the invitation token after registration. Update both auth Vue pages to pass the token through. ## Boundaries & Constraints **Always:** - Validate invitation via `TeamInvitation::isValid()` at every entry point - Attach user to workspace using role from invitation + default permissions from `config('permissions.defaults')` - Set `current_workspace_id` in session after acceptance - Mark invitation `accepted_at = now()` in a DB transaction - Show a dedicated error page for invalid/expired tokens **Ask First:** - Any changes to the invitation email subject or body beyond the URL **Never:** - Modify Fortify's core auth pipeline or middleware stack - Allow accepting an invitation for an email that doesn't match the authenticated user's email - Allow a user who is already a member of the workspace to re-accept ## I/O & Edge-Case Matrix | Scenario | Input / State | Expected Output / Behavior | Error Handling | |----------|--------------|---------------------------|----------------| | New user, valid token | Guest visits `/team/invitation/{token}/accept` | Redirect to `/register?invitation={token}` with email pre-filled and read-only | N/A | | New user completes registration | POST register with `invitation` field | User created, attached to workspace, session set, redirect to `/dashboard` | N/A | | Existing user, authenticated, valid token | Auth user visits `/team/invitation/{token}/accept` | Auto-accept: attach to workspace, set session, redirect to `/dashboard` with success flash | N/A | | Existing user, not authenticated | Guest with existing account visits entry route | Redirect to `/login?invitation={token}` with status message | N/A | | Post-login with invitation token | User logs in with `invitation` query param | Process invitation after login, redirect to dashboard | N/A | | Expired token | Any visitor | Dedicated error page: "Cette invitation a expiré" | Inertia error page | | Already-accepted token | Any visitor | Dedicated error page: "Cette invitation a deja ete utilisee" | Inertia error page | | Email mismatch (auth user email != invitation email) | Auth user visits entry route | Error flash: "Cette invitation est destinee a une autre adresse email" + redirect back | Redirect with error | | Already a workspace member | Auth user visits entry route | Flash "Vous etes deja membre de cet espace de travail" + redirect to dashboard | Redirect with info | ## Code Map - `app/Http/Controllers/TeamInvitationAcceptController.php` -- NEW: entry route controller, handles routing logic and acceptance for authenticated users - `app/Actions/Fortify/CreateNewUser.php` -- ADD: invitation processing after user creation - `app/Providers/FortifyServiceProvider.php` -- MODIFY: pass invitation data to register/login views, add custom registration response - `resources/js/pages/auth/Register.vue` -- MODIFY: accept invitation props, pre-fill email, add hidden field - `resources/js/pages/auth/Login.vue` -- MODIFY: accept invitation prop, pass token through form, show invitation message - `routes/web.php` -- ADD: `/team/invitation/{token}/accept` route - `app/Mail/TeamInvitationMail.php` -- MODIFY: change URL to new acceptance route - `resources/js/pages/auth/InvitationError.vue` -- NEW: dedicated error page for invalid/expired tokens - `app/Http/Responses/RegisterResponse.php` -- NEW: custom Fortify response to redirect to dashboard with workspace session after invitation registration ## Tasks & Acceptance **Execution:** - [ ] `app/Http/Controllers/TeamInvitationAcceptController.php` -- Create single-action controller with `__invoke()`: validate token, check auth state, check if email has existing account, route accordingly - [ ] `routes/web.php` -- Add `GET /team/invitation/{token}/accept` route (no auth middleware, publicly accessible) - [ ] `resources/js/pages/auth/InvitationError.vue` -- Create error page with `AuthLayout`, show message + link to home - [ ] `app/Providers/FortifyServiceProvider.php` -- Update `registerView` to pass `invitation` query param and invitation email as props; update `loginView` to pass `invitation` query param - [ ] `resources/js/pages/auth/Register.vue` -- Accept `invitation` and `invitationEmail` props, pre-fill email (read-only when invitation present), add hidden `invitation` input - [ ] `resources/js/pages/auth/Login.vue` -- Accept `invitation` prop, add hidden `invitation` input, show invitation status message - [ ] `app/Actions/Fortify/CreateNewUser.php` -- After user creation: if `invitation` token present, validate, attach workspace, mark accepted, set session - [ ] `app/Http/Responses/RegisterResponse.php` -- Create custom `RegisterResponse` implementing `Fortify\Contracts\RegisterResponse`: if session has `invitation_workspace_id`, redirect to `/dashboard`; bind in `FortifyServiceProvider` - [ ] `app/Mail/TeamInvitationMail.php` -- Change `registerUrl` to `route('team.invitation.accept', $this->invitation->token)` **Acceptance Criteria:** - Given a new user clicks the invitation email link, when they complete registration, then they are attached to the workspace with the correct role and land on the workspace dashboard - Given an authenticated user clicks the invitation link, when the token is valid and email matches, then they are auto-attached and redirected to the dashboard with the new workspace active - Given a non-authenticated user with an existing account clicks the link, when they log in, then the invitation is processed and they land on the workspace dashboard - Given an expired or used token, when any user visits the link, then they see a clear French-language error page - Given an email mismatch between authenticated user and invitation, when they visit the link, then they see an error and are not attached ## Verification **Commands:** - `composer test` -- expected: all existing tests pass (no regressions) - `npm run lint` -- expected: no lint errors - `composer lint` -- expected: no PHP formatting errors