Session resilience and SMTP-optional team invites - #4
Merged
Conversation
A 401 from any request now tears the session down cleanly: the shim clears the token, broadcasts SIGNED_OUT so ProtectedRoute redirects to login, toasts why, and stashes the current path so login returns you there. Both the QueryBuilder (every .from() call) and a new apiFetch wrapper funnel through one handler, deduped against concurrent 401s. Consolidates the hand-rolled fetch+Bearer call sites (invoice PDF, bulk PDF, bulk send, api-keys, change-password, bill-import) onto apiFetch, dropping their manual localStorage token reads and dead getSession calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The invite form now actually creates the user via /auth/users (with the selected role) and surfaces the tokenised /signup?token= link so the invitee can set their password - previously it only fired a token-less email that no one could act on, and did nothing at all when SMTP was unconfigured. The link is shown with a copy button whenever email is unavailable (or alongside a successful send), and /mail/send-invite now emails that same tokenised link instead of a bare /signup URL. Adds an auth test for the role_id invite create -> accept round trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two reliability fixes for problems a real user hits in their first week: expired sessions silently breaking the app, and team invites that don't work without SMTP.
1. Handle expired sessions instead of silently breaking (
9a3401d)Problem: When a JWT expired mid-session, DB calls just failed and the app didn't react — any half-filled form was lost with no explanation. Separately, ~6 call sites each hand-rolled
fetchwith a manually rebuiltAuthorization: Bearerheader and no 401 handling.Fix: A single
handleUnauthorized()in the API shim (src/integrations/api/client.ts) now reacts to any 401: clears the token, broadcastsSIGNED_OUT(which drivesProtectedRouteto redirect to/login), shows a toast explaining why, and stashes the current path insessionStorageso login returns you there. Both theQueryBuilder(every.from()DB call) and a new exportedapiFetch(path, options)wrapper funnel through it, deduped against concurrent 401s.Login.tsxconsumes the stashed path on success.The invoice-PDF, bulk-PDF, bulk-send, api-keys, change-password, and bill-import call sites were migrated onto
apiFetch, dropping their manuallocalStoragetoken reads and some deadgetSession()calls.2. Make team invites work without SMTP (
848a6ed)Problem: The invite form was broken beyond just SMTP — it never created the invited user, ignored the selected role, and emailed a link to
/signupwith no token, so the invitee couldn't accept even when email worked. With SMTP unconfigured (the default for a local-first app), inviting did nothing usable.Fix:
handleInvitenow creates the user viaPOST /auth/users(which mints the invite token and assigns the chosen role) and surfaces the real${origin}/signup?token=<token>link with a copy button — shown whenever email is unavailable, and alongside a successful send otherwise./mail/send-invitenow emails that same tokenised link instead of a bare/signup.Reviewer notes
SIGNED_OUTbroadcast path thatsignOut()already uses, so no router access is needed from the shim.apiFetchdeliberately does not force aContent-Type, so FormData callers still get the right multipart boundary.typecheckclean,buildpasses,test:run70/70 (a new auth test covers the role'd invite create → accept round trip). The occasionalbackup.test.tsfailure under full-suite parallel load is a pre-existing flake — it passes 3/3 in isolation.🤖 Generated with Claude Code