Skip to content

fix(dashboard): allow signed-out invitees to complete the invitation flow - #712

Open
bry4ns wants to merge 1 commit into
oblien:mainfrom
bry4ns:fix/accept-invite-logged-out
Open

fix(dashboard): allow signed-out invitees to complete the invitation flow#712
bry4ns wants to merge 1 commit into
oblien:mainfrom
bry4ns:fix/accept-invite-logged-out

Conversation

@bry4ns

@bry4ns bry4ns commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

The accept-invite page could not complete its primary use case on self-hosted invite-only instances: a user opening an invitation link without already having an account.

This PR fixes the invitation flow so signed-out invitees can reach the existing create-account flow, sign in automatically after account creation, and accept the invitation without the page falling back into an error state.

This complements #696, which made the route publicly accessible. After that change, visitors could reach the page, but the invitation flow still failed before signup.


Problem

Three issues combined to break the experience for first-time invitees:

  1. Invitation details were requested before checking for a session.

    organization.getInvitation requires an authenticated session. Visiting /accept-invite/[id] while signed out immediately failed with 401 Not authenticated, causing the page to render the "Invalid invitation" error instead of the signup UI. As a result, the existing !session signup branch was effectively unreachable.

  2. Invitation loading failed on the current self-hosted better-auth setup.

    On the tested better-auth v1.5.4 setup, the dashboard client issued a POST request for organization/get-invitation, while the server exposed the route as GET. The invitation fetch therefore failed even for authenticated users, while accept-invitation and reject-invitation continued working because those routes are POST.

  3. The page re-fetched the invitation after signup completed.

    Creating an account automatically signs the user in, which changes the session and re-runs the page effect. That second fetch occurs after the invitation has already been accepted, replacing the success UI with the "Can't accept this invite" error state.


Evidence

API request log of one clean incognito run through signup on a self-hosted deployment of current main:

POST /api/system/invite-signup                       200   account created
POST /api/auth/sign-in/email                         200   auto sign-in
POST /api/auth/organization/accept-invitation        200   accepted
POST /api/permissions/invitations/<id>/materialize   200
POST /api/auth/organization/get-invitation           404   client speaks POST…
GET  /api/auth/organization/get-invitation?id=…      401   …server only routes GET
                                                           (401 = no session in this
                                                            probe; route exists)

Both halves reproduced against the running stack:

$ curl -X POST …/api/proxy/api/auth/organization/get-invitation
→ 404                                  (route not registered for POST)

$ curl "…/get-invitation?id=<pending id>"             (no cookies)
→ 401 {"message":"Not authenticated"}  (route alive, session required)

Changes

File changed

apps/dashboard/src/app/accept-invite/[id]/page.tsx

Invitation flow

  • Check for an authenticated session before attempting to load invitation details.
  • Signed-out visitors now immediately see the existing Sign in / Create account choice instead of failing on a session-protected request.
  • The invitation token in the URL remains the only credential; /api/system/invite-signup continues to perform all server-side validation (pending status, expiration, inviter permissions, and email extraction).

Invitation loading

  • Load invitation details through the server's GET /auth/organization/get-invitation?id=... endpoint via the app's existing api client, matching the server route used by the current self-hosted setup.
  • Accept/reject actions continue using the existing better-auth client methods (those routes are POST and were already working).

State handling

  • Skip invitation re-fetches while an acceptance is already in progress or completed.
  • Ignore stale fetch results once the invitation has been accepted, preventing the success screen from being overwritten after the session updates.

Signup

  • After account creation, sign in using the email returned by /api/system/invite-signup instead of any client-derived value.

UI

  • Render "Join {organization} as {email}" only when invitation details are available.
  • Signed-out visitors get the existing generic localized invitation copy.
  • No locale files changed.

Why this is safe

  • No API changes.
  • No new endpoints.
  • No database changes.
  • No dependency changes.
  • All invitation validation remains server-side.

The client only changes when and how invitation metadata is requested.


Verification

Tested on a self-hosted Compose deployment built from this commit (API image untouched).

Before

  • Opening /accept-invite/[id] in an incognito window immediately rendered "Can't accept this invite / Invalid invitation".
  • The invitation endpoint failed before signup could be displayed; console showed …/api/proxy/api/auth/organization/get-invitation → 404.

After

  • The same invitation link displays "You're invited" with the existing Create account flow.
  • Creating an account creates the invited user, signs them in automatically, accepts the invitation, and redirects to the dashboard.
  • The success state remains visible after signup instead of being replaced by an error.

Regression checks

  • Signed in as a different account → existing wrong-account message still appears.
  • Expired or cancelled invitations → still rejected server-side during signup.
  • Signed-in invitees now load invitation details correctly (this path also hit the POST/GET mismatch before); wrong-account detection, status display and accept/reject keep their previous behavior.

Build

  • docker build -f apps/dashboard/Dockerfile . succeeds (includes type checking).

Checklist

  • Fixes a single user-facing bug — one change per PR, no unrelated formatting or refactoring.
  • Scoped to one file.
  • Existing signed-in behavior preserved.
  • Verified end-to-end on a self-hosted deployment.
  • Every line of the diff reviewed and understood by the author.

The accept-invite page called better-auth's get-invitation before checking
for a session, but that endpoint requires one (401 without it). A first-time
invitee therefore always landed in the error state ('Invalid invitation')
and could never see the inline sign-up form — the exact users the page
exists for on invite-only self-hosted instances.

Reorder the effect: signed-out visitors go straight to the sign-in /
create-account choice, and account creation signs in with the email the
server returns from /api/system/invite-signup (derived from the invitation
token) instead of any client-side value. Signed-in users keep the full
get-invitation flow (org/role details, wrong-account detection).

Before: GET /api/auth/organization/get-invitation -> 401 -> error state.
After: logged-out user sees the create-account form; signup + signin +
accept complete without touching session-gated endpoints.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant