Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions apps/website/src/pages/api/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,18 @@ const unavailable = (wantsJson: boolean) =>
? Response.json({ error: 'plan_unavailable' }, { status: 410 })
: new Response(null, { status: 303, headers: { location: '/#pricing' } });

// Scripts, scanners and uptime checks must not open live Stripe sessions: they pile up as expired
// checkouts with no email and make the funnel look like real people quit at the card step.
const isBot = (request: Request) => BOTS.test(request.headers.get('user-agent') ?? '');

export const POST: APIRoute = async ({ request, url }) => {
const wantsJson = (request.headers.get('accept') ?? '').includes('application/json');
let plan: Plan = 'monthly';
if (isBot(request)) {
return wantsJson
? Response.json({ error: 'not_a_browser' }, { status: 403 })
: new Response(null, { status: 303, headers: { location: '/#pricing' } });
}

try {
if (!(await allowed('CHECKOUT_LIMIT', request))) {
Expand Down Expand Up @@ -132,18 +141,18 @@ export const POST: APIRoute = async ({ request, url }) => {
export const GET: APIRoute = async (ctx) => {
if (RETIRED.has(ctx.url.searchParams.get('plan') ?? '')) return unavailable(false);
const plan = planFrom(ctx.url.searchParams.get('plan'));
if (BOTS.test(ctx.request.headers.get('user-agent') ?? '')) {
if (isBot(ctx.request)) {
return new Response(null, { status: 303, headers: { location: '/#pricing' } });
}
const form = new URLSearchParams({ plan });
for (const k of ATTR_KEYS) {
const v = ctx.url.searchParams.get(k);
if (v) form.set(k, v);
}
const request = new Request(ctx.request.url, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: form,
});
// Keep the visitor's IP and user agent so the rate limit and bot check still apply.
const headers = new Headers(ctx.request.headers);
headers.set('content-type', 'application/x-www-form-urlencoded');
headers.delete('content-length');
const request = new Request(ctx.request.url, { method: 'POST', headers, body: form });
return POST({ ...ctx, request });
};
Loading