Skip to content

Fix team invite redirect being lost after email verification#457

Merged
AnthonyRonning merged 2 commits intomasterfrom
devin/1772724946-fix-team-invite-redirect
Mar 5, 2026
Merged

Fix team invite redirect being lost after email verification#457
AnthonyRonning merged 2 commits intomasterfrom
devin/1772724946-fix-team-invite-redirect

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Mar 5, 2026

Fix team invite redirect lost after email verification

Summary

When a new user clicks a team invite link from email, they are redirected to sign up. After signing up, they should land on the team invite acceptance page — but this redirect was being lost in two ways:

  1. Email signup flow: The team invite page didn't render VerificationModal, so after signup the email verification popup would appear on the index page (or not at all), and after verifying, the user was stuck on / with no way back to the invite page.
  2. OAuth signup flow (GitHub/Google): The next parameter was never persisted to sessionStorage before redirecting to the OAuth provider, so it was lost entirely on the round-trip.

Changes

  • team.invite.$inviteId.tsx: Stores the invite URL in sessionStorage (post_auth_redirect) before redirecting to signup. Adds <VerificationModal /> so users can verify their email without leaving the invite page.
  • signup.tsx / login.tsx: Stores next in sessionStorage as post_auth_redirect before GitHub/Google OAuth web redirects.
  • auth.$provider.callback.tsx: Reads post_auth_redirect from sessionStorage after OAuth callback and navigates there instead of always going to /.
  • VerificationModal.tsx: After successful email verification, checks for post_auth_redirect and navigates to it.
  • verify.$code.tsx: After email verification via link, checks for post_auth_redirect and navigates there instead of always going to /.

Review & Testing Checklist for Human

  • Test the full email-signup invite flow end-to-end: Click invite link → sign up with email → verify email via modal → confirm you land on the invite acceptance page and can accept
  • Test the OAuth invite flow: Click invite link → sign up with GitHub or Google → confirm the OAuth callback redirects to the invite page (not /)
  • Verify VerificationModal placement: It's only rendered in the "invite preview" return path — check whether it should also appear in the loading/error states of the invite page
  • Check for stale post_auth_redirect: If a user abandons an invite flow and later signs in normally, verify they aren't unexpectedly redirected to an old invite URL
  • Verify navigate({ to: pendingRedirect }) works with TanStack Router: The redirect value is a dynamic string from sessionStorage — confirm TanStack Router handles paths like /team/invite/{uuid} correctly when passed as a string

Notes

  • Apple OAuth web flow and Tauri desktop flows were not updated to store post_auth_redirect — only GitHub and Google web flows. Apple web uses a popup (no page redirect), so the context is preserved. Tauri flows redirect to external browser and use deep links, which is a separate flow.
  • The sessionStorage key is scoped to the browser tab/session, so it won't leak across tabs or persist after the session ends.

Link to Devin session: https://app.devin.ai/sessions/3e6cfae8255e4610b40ae1e868bde04c
Requested by: @AnthonyRonning


Open with Devin

Summary by CodeRabbit

  • New Features
    • Users are now redirected to their originally intended destination after completing login, signup, or email verification—instead of defaulting to the home page. Redirects persist across OAuth flows and email verification workflows.

- Add VerificationModal to team invite page so users can verify email
  without losing their place in the invite flow
- Store post_auth_redirect in sessionStorage before OAuth redirects
  (GitHub/Google) so the invite URL survives the round-trip
- After email verification (modal or link), redirect to the stored
  URL instead of always going to /
- Read post_auth_redirect in OAuth callback to navigate to the
  correct page after authentication

Co-Authored-By: tony@opensecret.cloud <TonyGiorgio@protonmail.com>
@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 5, 2026

Deploying maple with  Cloudflare Pages  Cloudflare Pages

Latest commit: 128ea40
Status: ✅  Deploy successful!
Preview URL: https://834f7aad.maple-ca8.pages.dev
Branch Preview URL: https://devin-1772724946-fix-team-in.maple-ca8.pages.dev

View logs

Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

@AnthonyRonning
Copy link
Contributor

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c357ca74-56b6-4c21-aeac-5e3ecce3ba40

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes implement persistent post-auth redirect handling across the OAuth and email verification flows. When users initiate authentication, intended redirect paths are stored in sessionStorage before OAuth redirection, then retrieved and applied after authentication and verification complete.

Changes

Cohort / File(s) Summary
Post-auth Redirect Storage
frontend/src/routes/login.tsx, frontend/src/routes/signup.tsx
Store the next parameter in sessionStorage under post_auth_redirect before initiating OAuth flows for GitHub and Google, preserving the intended post-auth destination across redirects.
Post-auth Redirect Retrieval & Navigation
frontend/src/components/VerificationModal.tsx, frontend/src/routes/auth.$provider.callback.tsx, frontend/src/routes/verify.$code.tsx
Read post_auth_redirect from sessionStorage after successful authentication or email verification, navigate to the stored path if present, otherwise fall back to default routes (root or existing pricing logic).
Invite Flow Integration
frontend/src/routes/team.invite.$inviteId.tsx
Store the current invite URL in sessionStorage as post_auth_redirect when unauthenticated to preserve the invite context across OAuth redirects, and render VerificationModal on the invite page.

Sequence Diagram

sequenceDiagram
    actor User
    participant Browser
    participant SessionStorage
    participant OAuth as OAuth Provider
    participant Backend
    
    User->>Browser: Click "Sign up with OAuth"<br/>(with next=/invite/123)
    Browser->>SessionStorage: Store post_auth_redirect = "/invite/123"
    Browser->>OAuth: Redirect to OAuth provider
    
    User->>OAuth: Complete OAuth flow
    OAuth->>Browser: Redirect to auth callback
    Browser->>SessionStorage: Read post_auth_redirect
    Browser->>Backend: Verify user session
    Backend->>Browser: Return user data
    Browser->>Browser: Navigate to /invite/123
    Browser->>User: Render invite page
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Hop through the OAuth gate, store the path with care,
In sessionStorage's burrow, a destination fair,
Verify your email, the tunnel's complete,
Pop out precisely where intended to greet!

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main bug fix: preventing the loss of team invite redirects after email verification. It accurately reflects the core problem across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch devin/1772724946-fix-team-invite-redirect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
frontend/src/routes/auth.$provider.callback.tsx (1)

65-66: Harden redirect consumption with an internal-path guard.

Recommend validating post_auth_redirect again before navigation so stale/invalid storage values can’t break post-auth routing.

Proposed hardening
     const postAuthRedirect = sessionStorage.getItem("post_auth_redirect");
     sessionStorage.removeItem("post_auth_redirect");
+    const safePostAuthRedirect =
+      postAuthRedirect && postAuthRedirect.startsWith("/") && !postAuthRedirect.startsWith("//")
+        ? postAuthRedirect
+        : null;
@@
-      } else if (postAuthRedirect) {
-        navigate({ to: postAuthRedirect });
+      } else if (safePostAuthRedirect) {
+        navigate({ to: safePostAuthRedirect });
       } else {
         navigate({ to: "/" });
       }

Also applies to: 74-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/auth`.$provider.callback.tsx around lines 65 - 66, The
post-auth redirect consumption should validate that the sessionStorage value is
a safe internal path before navigating: when reading post_auth_redirect into the
postAuthRedirect variable (and the similar use at the other occurrence), check
it's a non-empty string and matches an internal-path guard (e.g., begins with
"/" and does not contain "//" or an absolute origin, or match your app's allowed
path regex) and only then call navigate; still remove the sessionStorage item
regardless to avoid reuse. Update the code around postAuthRedirect (and the
repeated block) to perform this validation and skip/redirect to a safe default
if validation fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/routes/login.tsx`:
- Around line 142-144: The code stores a user-controlled query param `next` into
sessionStorage as "post_auth_redirect" without validation; update both places
where sessionStorage.setItem("post_auth_redirect", next) is called to validate
`next` first: allow only safe internal paths (e.g., ensure `next` is a non-empty
string that starts with "/" but not "//", does not include a full origin, and
resolves to the same origin if parsed via new URL), otherwise do not persist it
(or fall back to "/"). Locate the occurrences using the `next` variable and the
exact call sessionStorage.setItem("post_auth_redirect", next) in this file and
replace with the validation logic described.

In `@frontend/src/routes/signup.tsx`:
- Around line 153-155: The code stores the raw URL param `next` into
sessionStorage via sessionStorage.setItem("post_auth_redirect", next) without
validating it's an internal path; mirror the login logic by validating `next`
with the same internal-path check used elsewhere (e.g., the helper used in the
login flow) before calling sessionStorage.setItem, and apply this fix to both
occurrences around the sessionStorage.setItem lines (the block with if (next) {
sessionStorage.setItem("post_auth_redirect", next); } and the similar block at
lines ~198-200) so only safe internal redirects are persisted.

In `@frontend/src/routes/verify`.$code.tsx:
- Around line 25-36: The setTimeout callback contains async work (await
refetchUser() and navigate) which escapes the surrounding try/catch and can
produce unhandled rejections; extract the async flow into a named async helper
(e.g., handlePostAuthRedirect or similar) that performs await refetchUser(),
reads sessionStorage ("post_auth_redirect"), removes it, and calls navigate(...)
inside its own try/catch, then invoke it from setTimeout using a non-async arrow
(e.g., setTimeout(() => void handlePostAuthRedirect(), 2000)) or call
handlePostAuthRedirect().catch(err => {/* log/handle error */}) so all errors
are caught and logged.

---

Nitpick comments:
In `@frontend/src/routes/auth`.$provider.callback.tsx:
- Around line 65-66: The post-auth redirect consumption should validate that the
sessionStorage value is a safe internal path before navigating: when reading
post_auth_redirect into the postAuthRedirect variable (and the similar use at
the other occurrence), check it's a non-empty string and matches an
internal-path guard (e.g., begins with "/" and does not contain "//" or an
absolute origin, or match your app's allowed path regex) and only then call
navigate; still remove the sessionStorage item regardless to avoid reuse. Update
the code around postAuthRedirect (and the repeated block) to perform this
validation and skip/redirect to a safe default if validation fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53d93b35-cfb8-44c0-8b02-bbeb8c93632c

📥 Commits

Reviewing files that changed from the base of the PR and between 200b09d and 5088d74.

📒 Files selected for processing (6)
  • frontend/src/components/VerificationModal.tsx
  • frontend/src/routes/auth.$provider.callback.tsx
  • frontend/src/routes/login.tsx
  • frontend/src/routes/signup.tsx
  • frontend/src/routes/team.invite.$inviteId.tsx
  • frontend/src/routes/verify.$code.tsx

Comment on lines +142 to +144
if (next) {
sessionStorage.setItem("post_auth_redirect", next);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate next before persisting it as post_auth_redirect.

This value is user-controlled (query param). Persisting it without an internal-path check can produce unsafe or malformed redirects later.

Proposed fix
+        const isSafeRedirect = (value: string) => value.startsWith("/") && !value.startsWith("//");
+
-        if (next) {
+        if (next && isSafeRedirect(next)) {
           sessionStorage.setItem("post_auth_redirect", next);
         }

Also applies to: 187-189

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/login.tsx` around lines 142 - 144, The code stores a
user-controlled query param `next` into sessionStorage as "post_auth_redirect"
without validation; update both places where
sessionStorage.setItem("post_auth_redirect", next) is called to validate `next`
first: allow only safe internal paths (e.g., ensure `next` is a non-empty string
that starts with "/" but not "//", does not include a full origin, and resolves
to the same origin if parsed via new URL), otherwise do not persist it (or fall
back to "/"). Locate the occurrences using the `next` variable and the exact
call sessionStorage.setItem("post_auth_redirect", next) in this file and replace
with the validation logic described.

Comment on lines +153 to +155
if (next) {
sessionStorage.setItem("post_auth_redirect", next);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Apply the same internal-path validation for next before storing redirect state.

Same concern as login: next comes from URL search params and should be constrained before persisting as post_auth_redirect.

Proposed fix
+        const isSafeRedirect = (value: string) => value.startsWith("/") && !value.startsWith("//");
+
-        if (next) {
+        if (next && isSafeRedirect(next)) {
           sessionStorage.setItem("post_auth_redirect", next);
         }

Also applies to: 198-200

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/signup.tsx` around lines 153 - 155, The code stores the
raw URL param `next` into sessionStorage via
sessionStorage.setItem("post_auth_redirect", next) without validating it's an
internal path; mirror the login logic by validating `next` with the same
internal-path check used elsewhere (e.g., the helper used in the login flow)
before calling sessionStorage.setItem, and apply this fix to both occurrences
around the sessionStorage.setItem lines (the block with if (next) {
sessionStorage.setItem("post_auth_redirect", next); } and the similar block at
lines ~198-200) so only safe internal redirects are persisted.

Comment on lines 25 to 36
setTimeout(async () => {
await refetchUser();
navigate({ to: "/" });

// Check for a pending redirect (e.g. team invite page)
const pendingRedirect = sessionStorage.getItem("post_auth_redirect");
if (pendingRedirect) {
sessionStorage.removeItem("post_auth_redirect");
navigate({ to: pendingRedirect });
} else {
navigate({ to: "/" });
}
}, 2000);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid async side effects inside setTimeout here.

refetchUser()/redirect failures inside the timer bypass the outer try/catch, so failures become unhandled and the flow can silently stall.

Proposed fix
-        // Do both refetch and navigation after a delay
-        setTimeout(async () => {
-          await refetchUser();
-
-          // Check for a pending redirect (e.g. team invite page)
-          const pendingRedirect = sessionStorage.getItem("post_auth_redirect");
-          if (pendingRedirect) {
-            sessionStorage.removeItem("post_auth_redirect");
-            navigate({ to: pendingRedirect });
-          } else {
-            navigate({ to: "/" });
-          }
-        }, 2000);
+        await new Promise((resolve) => setTimeout(resolve, 2000));
+        await refetchUser();
+
+        const pendingRedirect = sessionStorage.getItem("post_auth_redirect");
+        if (pendingRedirect) {
+          sessionStorage.removeItem("post_auth_redirect");
+          navigate({ to: pendingRedirect });
+        } else {
+          navigate({ to: "/" });
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/routes/verify`.$code.tsx around lines 25 - 36, The setTimeout
callback contains async work (await refetchUser() and navigate) which escapes
the surrounding try/catch and can produce unhandled rejections; extract the
async flow into a named async helper (e.g., handlePostAuthRedirect or similar)
that performs await refetchUser(), reads sessionStorage ("post_auth_redirect"),
removes it, and calls navigate(...) inside its own try/catch, then invoke it
from setTimeout using a non-async arrow (e.g., setTimeout(() => void
handlePostAuthRedirect(), 2000)) or call handlePostAuthRedirect().catch(err =>
{/* log/handle error */}) so all errors are caught and logged.

Validate that post_auth_redirect values start with '/' and not '//'
to prevent open redirect attacks via user-controlled query params.
Applied at both storage (login/signup) and consumption (callback,
verification modal, verify link) points.

Co-Authored-By: tony@opensecret.cloud <TonyGiorgio@protonmail.com>
@AnthonyRonning AnthonyRonning merged commit 7bf6d61 into master Mar 5, 2026
12 checks passed
@AnthonyRonning AnthonyRonning deleted the devin/1772724946-fix-team-invite-redirect branch March 5, 2026 18:01
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