Skip to content

Validate invite codes before showing the signup form#1265

Merged
RhysSullivan merged 1 commit into
mainfrom
ux/join-invite-validation
Jul 2, 2026
Merged

Validate invite codes before showing the signup form#1265
RhysSullivan merged 1 commit into
mainfrom
ux/join-invite-validation

Conversation

@RhysSullivan

Copy link
Copy Markdown
Owner

Visiting /join/ with an invalid or expired code rendered the full You've been invited signup form and only failed after the user filled it in and submitted.

The join page now validates the code on mount via a new lightweight GET invite-status endpoint (read-only, does not burn the code). While checking it shows the loading state; an invalid code shows an Invite not valid card with no form.

Verified with an extended invites test covering the new endpoint plus manual browser runs for valid and invalid codes. Typecheck is green.

Stacked on #1264.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds upfront invite-code validation to the /join/<code> page by introducing a lightweight, read-only GET /api/invite-status/:code endpoint and wiring it to a useEffect that gates the signup form behind a loading state and a dead-end "Invite not valid" card for bad codes.

  • New endpoint (api.ts + handlers.ts): calls findRedeemableCode (the same DB query used at signup) and returns { valid: boolean } without consuming the code; the existing SystemError type handles DB failures.
  • Frontend guard (join.$code.tsx): three-state machine (checkingvalid | invalid) prevents the signup form from ever rendering for an expired or nonexistent code.
  • Test coverage (invites.node.test.ts): the existing invite-redemption test is extended with an inviteStatus helper and three assertions covering a bogus code, a fresh code, and a post-redemption code.

Confidence Score: 4/5

Safe to merge once the open discussion about transient server errors rendering the "Invite not valid" dead-end is resolved or accepted as a known trade-off.

The backend change is minimal and read-only, the three-state guard on the frontend is correct, and the test coverage is solid. The one open thread on this PR identifies a real UX defect in the error-handling path — a DB hiccup or brief network blip silently maps to the same dead-end card shown for a genuinely invalid code, giving a legitimate invite holder no way to retry. Until that is addressed or explicitly accepted, the change is not fully production-ready.

apps/host-selfhost/web/routes/public/join.$code.tsx — the fetch rejection and non-2xx response paths both resolve to the same "Invite not valid" state; the open thread on this file describes the concern in detail.

Important Files Changed

Filename Overview
apps/host-selfhost/web/routes/public/join.$code.tsx Adds useEffect-driven invite code pre-validation on mount; shows loading/invalid states before rendering the signup form. Network and server errors collapse into the "invalid" state (flagged in a previous thread).
apps/host-selfhost/src/system/api.ts Adds InviteStatusResponse schema and the GET /invite-status/:code endpoint definition to SystemApi; clean addition consistent with existing pattern.
apps/host-selfhost/src/system/handlers.ts Implements the inviteStatus handler using findRedeemableCode; correctly returns { valid: false } for expired/used/nonexistent codes and { valid: true } for redeemable ones.
apps/host-selfhost/src/admin/invites.node.test.ts Extends the existing invite test with an inviteStatus helper and three targeted assertions: bogus code → false, freshly minted → true, used code → false.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Browser
    participant JoinPage
    participant InviteStatusAPI as GET /api/invite-status/:code
    participant DB as invite_code table

    Browser->>JoinPage: "Navigate to /join/<code>"
    JoinPage->>JoinPage: setState("checking") → show Loading…
    JoinPage->>InviteStatusAPI: "fetch /api/invite-status/<code>"
    InviteStatusAPI->>DB: "SELECT * WHERE code=? AND used_at IS NULL"
    DB-->>InviteStatusAPI: row or null
    InviteStatusAPI-->>JoinPage: "{ valid: true|false } (HTTP 200)"

    alt "valid === true"
        JoinPage->>JoinPage: setState("valid") → show signup form
        Browser->>JoinPage: Fill form and submit
        JoinPage->>JoinPage: authClient.signUp.email(...)
        JoinPage-->>Browser: Redirect to "/"
    else "valid === false"
        JoinPage->>JoinPage: setState("invalid") → show "Invite not valid" card
    else "fetch rejected or response.ok===false"
        JoinPage->>JoinPage: setState("invalid") → show "Invite not valid" card
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Browser
    participant JoinPage
    participant InviteStatusAPI as GET /api/invite-status/:code
    participant DB as invite_code table

    Browser->>JoinPage: "Navigate to /join/<code>"
    JoinPage->>JoinPage: setState("checking") → show Loading…
    JoinPage->>InviteStatusAPI: "fetch /api/invite-status/<code>"
    InviteStatusAPI->>DB: "SELECT * WHERE code=? AND used_at IS NULL"
    DB-->>InviteStatusAPI: row or null
    InviteStatusAPI-->>JoinPage: "{ valid: true|false } (HTTP 200)"

    alt "valid === true"
        JoinPage->>JoinPage: setState("valid") → show signup form
        Browser->>JoinPage: Fill form and submit
        JoinPage->>JoinPage: authClient.signUp.email(...)
        JoinPage-->>Browser: Redirect to "/"
    else "valid === false"
        JoinPage->>JoinPage: setState("invalid") → show "Invite not valid" card
    else "fetch rejected or response.ok===false"
        JoinPage->>JoinPage: setState("invalid") → show "Invite not valid" card
    end
Loading

Reviews (3): Last reviewed commit: "Validate join invites before showing sig..." | Re-trigger Greptile

Comment on lines +44 to 53
() => {
if (alive) setInviteState("invalid");
},
);
return () => {
alive = false;
};
}, [code]);

const submit = async (event: FormEvent) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Network / server errors silently shown as "Invite not valid"

Both a rejected fetch (network down, DNS failure) and a non-2xx response (DB error returning a SystemError) collapse into setInviteState("invalid"), which renders the "Invite not valid" card. A user holding a perfectly good invite code who visits during a brief server hiccup will be told their link is expired or invalid and given no way to retry. The fetch-rejection branch and the !response.ok body-override both need to distinguish between "the server said invalid" and "we couldn't reach the server at all" — a separate "error" state with a retry prompt would prevent the misleading dead-end.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 2c1d30c Jul 02 2026, 08:15 PM

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 2c1d30c Commit Preview URL

Branch Preview URL
Jul 02 2026, 08:13 PM

@RhysSullivan RhysSullivan force-pushed the ux/join-invite-validation branch from b0f4737 to bc8b745 Compare July 2, 2026 17:52
@RhysSullivan RhysSullivan force-pushed the ux/integration-detail-unlock-cta branch 2 times, most recently from 2808d62 to f867dbe Compare July 2, 2026 17:55
@RhysSullivan RhysSullivan force-pushed the ux/join-invite-validation branch from bc8b745 to f1afa78 Compare July 2, 2026 17:55
@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1265

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1265

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1265

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1265

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1265

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1265

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1265

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1265

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1265

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1265

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1265

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1265

executor

npm i https://pkg.pr.new/executor@1265

commit: f1afa78

@RhysSullivan RhysSullivan force-pushed the ux/integration-detail-unlock-cta branch from f867dbe to 62d477a Compare July 2, 2026 18:20
@RhysSullivan RhysSullivan force-pushed the ux/join-invite-validation branch 2 times, most recently from 2695ec3 to a521c37 Compare July 2, 2026 18:21
@RhysSullivan RhysSullivan force-pushed the ux/integration-detail-unlock-cta branch 2 times, most recently from 9ae67c8 to b0037e9 Compare July 2, 2026 18:21
@RhysSullivan RhysSullivan force-pushed the ux/join-invite-validation branch from a521c37 to e977b28 Compare July 2, 2026 18:21
@RhysSullivan RhysSullivan force-pushed the ux/join-invite-validation branch from e977b28 to 2c1d30c Compare July 2, 2026 18:21
@RhysSullivan RhysSullivan changed the base branch from ux/integration-detail-unlock-cta to main July 2, 2026 18:21
@RhysSullivan RhysSullivan merged commit 2f8d7b6 into main Jul 2, 2026
18 of 21 checks passed
@RhysSullivan RhysSullivan deleted the ux/join-invite-validation branch July 2, 2026 18:23
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