Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/tech-spec-team-invitation-acceptance.md
Saad Zoubir 88e5803061 feat: add team invitation acceptance flow with email link routing
Implement end-to-end invitation acceptance: neutral entry route validates
token and routes to register (new users), login (existing users), or
auto-accepts (authenticated users). Handles 2FA token survival via
session, email case-insensitive matching, and dedicated error pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 15:16:45 +01:00

6.7 KiB

title, type, created, status, baseline_commit, context
title type created status baseline_commit context
Team Invitation Acceptance Flow feature 2026-03-27 done 8f39bd9
_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