Commit Graph

4 Commits

Author SHA1 Message Date
5e0c9d2de1 fix(auth): store OAuth refresh token JTI to fix /auth/refresh after OAuth signup
OAuth callbacks (POST /auth/google/callback, POST /auth/microsoft/callback)
issued refresh tokens via create_refresh_token() but never persisted the JTI
in the refresh_tokens table. The /auth/refresh rotation logic does a
conditional UPDATE that requires a matching unrevoked row; without it the
first refresh attempt 401s with "Refresh token has been revoked" and OAuth
users get effectively logged out after the ~5 minute access-token expiry.

- Promote _store_refresh_token to module-public store_refresh_token in
  app.api.endpoints.auth (existing callers in /login, /login/json, /refresh
  updated in-place — same module, just renamed).
- OAuth callbacks now call store_refresh_token(...) + db.commit() after
  _sign_in_or_register returns. _sign_in_or_register already commits the
  user/account/identity rows; the refresh-token row gets its own commit.
- Tests:
  - test_oauth_google_callback_stores_refresh_token_jti — asserts the JTI
    hash is in refresh_tokens after a Google callback.
  - test_oauth_refresh_works_after_oauth_signup — full e2e: callback -> use
    returned refresh token at /auth/refresh -> 200 with rotated tokens.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 01:30:14 -04:00
39e85c9770 feat(auth): add /accept-invite page + lookup endpoint
Adds the invitee-side flow for self-serve signup Phase 2 (Task 36):

Backend
- Public GET /accounts/invites/{code}/lookup returns
  {account_name, inviter_name, invited_email, role} for a valid invite,
  404 invite_invalid_or_expired_or_revoked otherwise (collapses unknown /
  expired / revoked / used into one anti-enumeration response). Mounted
  in a new account_invite_lookup endpoints module on the public route
  list, uses get_admin_db (BYPASSRLS) since the caller has no tenant.
- OAuthCallbackPayload gains optional account_invite_code + invited_email.
  _sign_in_or_register honors them: a new OAuth user with a valid invite
  joins the invited account (no personal account, no Pro trial), the
  invite is marked used, and OAuth-profile-email vs invite-email mismatch
  raises invite_email_mismatch (matching the email+password register
  contract).

Frontend
- New public route /accept-invite -> AcceptInvitePage. Reads ?code=,
  calls inviteApi.lookupAccountInvite, renders "Join {account} on
  ResolutionFlow" with the invited email locked (rendered as a div, not
  an input), three sign-in options (set password, Google, Microsoft),
  and a clear "ask {inviter} to resend" + mailto: fallback for invalid
  codes.
- OAuth state for invitees is base64url(JSON({csrf, accountInviteCode,
  invitedEmail})). OAuthCallbackPage decodes both shapes, forwards the
  invite fields to the backend, and surfaces invite_email_mismatch /
  invite_invalid_or_expired_or_revoked errors with friendly text.
  Successful invite-OAuth lands on /?welcome=teammate (suppresses the
  welcome wizard for invitees per spec).
- UserCreate type + invite/auth API clients extended for the new fields.

Tests
- Backend: invite lookup happy path + four invalid-state collapse, OAuth
  callback links invite when supplied + rejects on email mismatch.
- Frontend Vitest: AcceptInvitePage renders account name + locked email
  + accept buttons; resend message + mailto on invalid code.

All 43 backend auth/account/invite/email-verification tests green;
frontend Vitest 120/120 green; tsc -b clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:34:22 -04:00
2ef2350de7 feat(auth): add Microsoft OAuth callback
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 19:14:30 -04:00
f4606f073a feat(auth): add Google OAuth callback with oauth_identities linking
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 19:14:30 -04:00