Skip to content

fix(ui): render the sign-in form on self-hosted deploys instead of looping (#311) - #319

Open
stubbi wants to merge 1 commit into
mainfrom
fix/self-hosted-sign-in-route
Open

fix(ui): render the sign-in form on self-hosted deploys instead of looping (#311)#319
stubbi wants to merge 1 commit into
mainfrom
fix/self-hosted-sign-in-route

Conversation

@stubbi

@stubbi stubbi commented Aug 5, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • It ships in several deployment shapes; two matter here — our hosted cloud, where a gateway sits in front of the SPA and reserves /auth/*, and a self-hosted authenticated install, where the SPA itself is the only thing serving auth
  • Fork PR fix(cloud): sign-out and unauthenticated redirects leave the SPA to the gateway auth pages #273 made the SPA hand /auth off to the gateway, which is correct on cloud and wrong on every other deployment, because it removed the only route that rendered the sign-in form
  • On self-hosted installs /auth/sign-in now resolves to nothing, falls through to the catch-all, and CloudAccessGate redirects the unauthenticated visitor to the page they are already on — nesting next until the URL hits HTTP 414
  • The effect is that nobody can log into a self-hosted authenticated install at all, on every image since 2026-07-20
  • This pull request stops the gate from redirecting auth routes to themselves, and routes /auth/sign-in to the AuthPage component that was left in the tree but unrouted
  • The benefit is that self-hosters can log in again, and the loop cannot be reintroduced by adding another auth subroute later

Linked Issues or Issue Description

Fixes #311.

Reported by @vfoschi with a clean-profile Playwright repro and a bisect to the exact image revision (works b57c11d7, broken e02ab855 and later). That bisect is what made the cause quick to confirm — thank you.

What Changed

  • ui/src/components/CloudAccessGate.tsx — the unauthenticated redirect now skips auth routes (isAuthRoute()). Being signed out on the sign-in page is the expected state, not a reason to redirect. This is the general guard: it holds for any future /auth/* subroute.
  • ui/src/App.tsx/auth/sign-in is routed to AuthPage. Inert on cloud, where the gateway reserves /auth/* and the request never reaches the bundle; on self-hosted it is what actually renders the form once the loop stops.
  • ui/src/components/CloudAccessGate.test.tsx — the useLocation mock is now per-test mutable (default unchanged), plus two regression tests.

Either change alone stops the 414. Neither alone is sufficient: the guard without the route leaves you on a page that renders nothing, and the route without the guard still loops for any other unauthenticated path that lands under /auth/.

Root cause

8695d8854 (PR #273) changed one line:

-        <Route path="auth" element={<AuthPage />} />
+        <Route path="auth" element={<CloudAuthRedirect />} />

CloudAuthRedirect unconditionally runs window.location.replace("/auth/sign-in?next=…"). With no SPA route for /auth/sign-in, the catch-all takes it, and the gate ran:

const next = encodeURIComponent(`${location.pathname}${location.search}`);
window.location.replace(`/auth/sign-in?next=${next}`);

with no exemption for the sign-in route itself. Upstream paperclipai/paperclip still routes AuthPage and is unaffected — this is a fork-only regression, which is why it survived two weeks: it cannot reproduce on the only deployment we look at.

Verification

pnpm vitest run ui/src/components/CloudAccessGate.test.tsx   # 5 passed (2 new)
pnpm vitest run ui/src                                       # 3171 passed

Both new tests were confirmed to fail before the fix and pass after.

Full-suite comparison against a clean fork/main checkout on the same machine: 16 test files fail at baseline, the same 16 with this change, identical sets. No regressions.

Honest gap: the two new tests cover the loop guard, which is the 414. They do not cover "the form actually mounts at /auth/sign-in" end to end, because that needs a real self-hosted authenticated deployment. There is no regression test today for anonymous user can reach the sign-in form in authenticated mode, and that absence is why this shipped. I have asked @vfoschi for their Playwright script as a basis for one; that belongs in the e2e suite as a follow-up, not in this PR.

Risks

Low on cloud: the new route is unreachable there (gateway-reserved), and the gate guard only changes behaviour for paths starting /auth/, which the SPA never renders on cloud. Cloud sign-out and unauthenticated redirect behaviour from #273 is untouched.

The one behaviour change worth naming: an unauthenticated visitor who lands on an /auth/* path that has no route will now fall through to the catch-all and render the app shell rather than redirecting. That is strictly better than an infinite redirect, but it is a 404-shaped experience. /auth and /auth/sign-in — the only two auth URLs the app produces — both have routes.

Model Used

  • Claude Opus 5 (claude-opus-5[1m], 1M context), extended thinking, via the Claude Code CLI harness with tool use (shell, file edits). Change authored agent-assisted.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have not referenced internal/instance-local Paperclip issues or links (only public GitHub #NNN / github.com/paperclipai/paperclip URLs)
  • My branch name describes the change (e.g. docs/..., fix/...) and contains no internal Paperclip ticket id or instance-derived details
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups
  • I will address all Greptile and reviewer comments before requesting merge

🤖 Generated with Claude Code

…oping

Fixes #311.

PR #273 replaced the SPA's only auth route with a redirect to the
gateway-served page:

    -  <Route path="auth" element={<AuthPage />} />
    +  <Route path="auth" element={<CloudAuthRedirect />} />

That is right on cloud, where the gateway reserves /auth/* and the SPA
never sees those URLs. Everywhere else it is wrong: nothing serves
/auth/sign-in, so it falls through to the catch-all, CloudAccessGate sees
an unauthenticated visitor and redirects to /auth/sign-in?next=<current>
-- which is the page it is already on. `next` nests one level per pass
until the request line hits HTTP 414. The form never mounts, so nobody
can log into a self-hosted install in `authenticated` mode. AuthPage was
never deleted, just left unrouted.

Two changes, either of which alone would stop the 414 but neither of
which alone is sufficient:

- CloudAccessGate no longer redirects when the visitor is already on an
  auth route. Being unauthenticated on the sign-in page is the normal
  state, not a reason to redirect. This is the general guard, so a future
  auth subroute cannot reintroduce the loop.
- /auth/sign-in is routed to AuthPage, so the form actually renders once
  the loop stops. Inert on cloud, where the request never reaches the
  bundle.

Reported by @vfoschi with a clean-profile Playwright repro and a bisect
to the exact image revision, which is what made this quick to pin down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Login page enters infinite redirect loop (/auth/sign-in?next=… → HTTP 414), sign-in form never renders (authenticated mode)

1 participant