fix(frontend): Legal accept and deep links 404 on /vote at vote.cl8y.com #8
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Live QA on
https://vote.cl8y.com(Terra Classic, mobile in-app browser): after Accept Terms, the Legal portal returns the user tohttps://vote.cl8y.com/voteand the host responds 404. Stripping/vote(landing on/) loads the dApp, restores the SPA, and the wallet stays connected.This is a production-blocking loop: every connected user must leave the origin for Legal, then come back via a full document load of a client-only route.
Related: #5 (Legal gate), #7 (Coolify / live QA). Do not treat this as “ops only” — in-tree routing and the Coolify nginx snippet both need to make
/votea 200 HTML document.Current codebase
Standalone voting dApp (
frontend/) is hosted on its own hostname (vote.cl8y.com) but still uses the DEX-era path prefix:frontend/src/App.tsx/→<Navigate to="/vote" replace />. Real pages:/vote,/vote/new,/vote/:id.frontend/src/components/layout/AppLayout.tsxConnectedTermsGatewraps<Outlet />.frontend/src/components/legal/ConnectedTermsGate.tsxredirectUri={resolveLegalRedirectUri()}(currentwindow.location.href).frontend/src/utils/legalClickwrap.tshttps://vote.cl8y.com. Paths such ashttps://vote.cl8y.com/voteare allowed.@plasticdigits/cl8y-clickwrapTermsGatewindow.location.href = buildSignUrl(..., { redirectUri }). Portal later full-navigates back to that URI.deploy/docker/frontend.nginx.conflocation / { try_files $uri $uri/ /index.html; }— correct if this file is what actually serves production.deploy/docker/frontend.DockerfileGET /. Never probes/vote.baseis/(not/vote/).Client-side
<Link to="/vote">works. Reload, Legal return, and shared URLs hit the HTTP server. If Coolify, Traefik, or a parent nginx does not SPA-fallback,/voteis a missing file → 404./servesindex.html, React then client-redirects to/vote, which matches the reported workaround.Why this is needed
Legal is required (fail closed after connect). The happy path is:
vote.cl8y.com.terms.cl8y.comwithredirect_uri.If step 3 404s, users cannot reach register / propose / vote without manually editing the URL. Mobile in-app browsers make that worse.
/vote/newand/vote/:idwill 404 on refresh the same way.The
/voteprefix was designed when voting was going to live inside the DEX origin. The product is now a dedicated host; the prefix is leftover and collides with Legal’s full-page return.Constraints / guardrails
redirect_uriallowlisting. Origins only (plus localhost in non-prod). Rejectjavascript:,data:, protocol-relative, and attacker hosts. Portal remain authoritative.ADMIN_TOKENin this frontend.connect-srcLegal API + terms + operator-voting). No blankethttps:.VITE_PLAYWRIGHT_E2EorVITE_DEV_MNEMONICon Coolify.index.htmlfor all app routes over hiding the bug by only redirecting Legal to/. Refresh of/vote/newmust still work.Relevant files
frontend/src/App.tsxfrontend/src/components/legal/ConnectedTermsGate.tsxfrontend/src/utils/legalClickwrap.tsfrontend/src/utils/__tests__/legalClickwrap.test.tsfrontend/src/vendor/cl8y-clickwrap.ts(fallbackbuildSignUrl/sanitizeRedirectUri)deploy/docker/frontend.nginx.confdeploy/docker/frontend.Dockerfiledeploy/docker/frontend.security-headers.confdocs/FRONTEND.md,docs/OPS.md§2,skills/AGENTS_LEGAL_CLICKWRAP.mdSibling (do not implement voting there):
cl8y-ecosystem-legalportalVITE_REDIRECT_URI_ALLOWLISTandweb/src/redirect.ts.Recommended direction
vote.cl8y.com. If Coolify is not usingfrontend.Dockerfile+frontend.nginx.conf, either switch to that image or stamp the sametry_fileson the actual edge. Confirm no parent proxy 404s/votebefore nginx.GET /,GET /vote,GET /vote/newmust return 200text/html(SPAindex.html), not 404. Extend the image HEALTHCHECK or add a Coolify HTTP check for/vote.redirect_urionce deep links work. Optional: also allowhttps://vote.cl8y.com/so a broken edge still recovers. Do not send users to a different origin./,/new,/:idon this dedicated host (keep/vote*as aliases). Hostname already says vote; the extra prefix is what Legal round-trips. Only do this if SPA fallback is also fixed — aliases without fallback still 404.index.htmland React renders the list/new/detail routes.Acceptance criteria
signed_latest./vote,/vote/new, and/vote/:idreturns the app, not 404.https://vote.cl8y.com/still works (today it client-redirects to/vote)./and/votestill open; connected unsigned still fail-closed behind TermsGate.redirect_uristill does not navigate (portal + SDK tests remain green).VITE_*BSC RPC.Test plan (all paths)
HTTP / deploy
curl -sI https://vote.cl8y.com/→ 200 HTML.curl -sI https://vote.cl8y.com/vote→ 200 HTML (not 404).curl -sI https://vote.cl8y.com/vote/new→ 200 HTML.curl -sI https://vote.cl8y.com/vote/does-not-exist-uuid→ 200 HTML (SPA), then React empty/error — not nginx 404 for the document.curl -sI https://vote.cl8y.com/assets/<missing>.js→ 404 (statictry_files $uri =404must stay).Legal
network=EVM./vote/new(if gate wraps that route): return to new-proposal, not 404.https://vote.cl8y.comonly.App routes after return
/vote→/vote/new→ back still works.Regression
cd frontend && npm test(legalClickwrap allowlist / sanitize).cd frontend && npm run test:e2e(5 Playwright workers). Hatch remains webServer-only.Test plan (attack, hack, abuse)
redirect_uri=https://evil.example/must not auto-navigate after Legal accept. Same forjavascript:,data:,//evil, userinfo, http non-loopback.https://vote.cl8y.comis OK; a lookalike host (vote.cl8y.com.evil,https://vote.cl8y.com@evil) is not.redirect_urifor an attacker origin./votemust not be cached as the lasting representation once fallback is fixed (index.htmlno-cache already in nginx)..(js|css|…)location (404 without fallback).connect-src https:to “fix” Legal. NoADMIN_TOKENin the image./voteHTML.Verification criteria
vote.cl8y.com: Accept Terms in a real Terra mobile in-app browser and a desktop Keplr session; both return to a rendered proposals page without URL editing.deploy/docker/frontend.nginx.conf(or an equivalenttry_files) and thatcurlto/voteis 200./vote404s on a hard navigation.mentioned in issue #9
mentioned in issue #7
mentioned in merge request !19
!19 is merged to
main(no automerge, did not wait for CI). GitLab pipeline on !19 failed withci_quota_exceeded— jobs never started; not a product defect. Local sanity after merge: frontend unit 55, Playwright 9/9 (5 workers),deploy/docker/test-frontend-spa-fallback.shOK.In-tree (this does not close the issue): canonical routes
/,/new,/:idwith/vote*aliases; nginxtry_files $uri /index.htmlplus/assets/=404; HEALTHCHECK probes/voteand/new; Legalredirect_uristays path-preserving. Guardrails (allowlist, CSP, hatch/RPC refuse) intact.Still required before close:
signed_latest./vote/newand a proposal URL after the flattened JS is deployed.try_files(live host wasnginx/1.31.4, not the in-tree 1.27 image). HTML responses still lackedX-Frame-Options/ CSP /Cache-Control: no-cachethatfrontend.security-headers.confwould stamp.Live
curlofhttps://vote.cl8y.com/votewas already 200 SPA HTML at review time; that does not substitute for Legal Accept QA.mentioned in issue #12
Users reporting fixed, closing
mentioned in issue #16