Skip to content

Session resilience and SMTP-optional team invites - #4

Merged
Coriana merged 2 commits into
mainfrom
claude/project-usability-review-ad4940
Jul 12, 2026
Merged

Session resilience and SMTP-optional team invites#4
Coriana merged 2 commits into
mainfrom
claude/project-usability-review-ad4940

Conversation

@Coriana

@Coriana Coriana commented Jul 12, 2026

Copy link
Copy Markdown
Owner

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 fetch with a manually rebuilt Authorization: Bearer header 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, broadcasts SIGNED_OUT (which drives ProtectedRoute to redirect to /login), shows a toast explaining why, and stashes the current path in sessionStorage so login returns you there. Both the QueryBuilder (every .from() DB call) and a new exported apiFetch(path, options) wrapper funnel through it, deduped against concurrent 401s. Login.tsx consumes 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 manual localStorage token reads and some dead getSession() 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 /signup with 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: handleInvite now creates the user via POST /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-invite now emails that same tokenised link instead of a bare /signup.

Reviewer notes

  • The 401 teardown reuses the exact SIGNED_OUT broadcast path that signOut() already uses, so no router access is needed from the shim.
  • apiFetch deliberately does not force a Content-Type, so FormData callers still get the right multipart boundary.
  • Verified live end-to-end: forced a genuine 401 (token cleared, path stashed, redirect + toast, login restored the page); created an invite with SMTP off (user + copyable tokenised link), accepted it (auto-login), confirmed the token is single-use, and cleaned up the test user.
  • Gates: server typecheck clean, build passes, test:run 70/70 (a new auth test covers the role'd invite create → accept round trip). The occasional backup.test.ts failure under full-suite parallel load is a pre-existing flake — it passes 3/3 in isolation.

🤖 Generated with Claude Code

Coriana and others added 2 commits July 12, 2026 21:58
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>
@Coriana
Coriana merged commit e354f0a into main Jul 12, 2026
2 checks passed
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