---
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

<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">

## 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 |

</frozen-after-approval>

## 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
