- {i18n.t("black.subscribe.failurePrefix")} {props.message} -
-{i18n.t("black.subscribe.success.title")}
-{i18n.t("black.subscribe.success.chargeNotice")}
-{i18n.t("black.subscribe.title")}
-
- ${planData.id}{" "}
- {i18n.t("black.price.perMonth")}
-
{i18n.t("black.subscribe.paymentMethod")}
- -- {selectedWorkspace() - ? i18n.t("black.subscribe.loadingPaymentForm") - : i18n.t("black.subscribe.selectWorkspaceToContinue")} -
-- {i18n.t("black.finePrint.beforeTerms")} ยท{" "} - {i18n.t("black.finePrint.terms")} -
- - - ) -} diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index 6c7d739c0e9..b52028814ba 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -24,6 +24,7 @@ import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time" import { createReferralFromCookie } from "~/lib/referral-invite" import { getRequestEvent } from "solid-js/web" import { countryFromRequest } from "~/lib/request-country" +import { checkCheckoutRateLimit } from "~/routes/zen/util/redis" import { IconAlipay, IconChevron, IconUpi } from "~/component/icon" import { buildLiteUsageBreakdown, getModelQuotaLimit, getUsagePercent } from "~/lib/lite-usage" @@ -219,6 +220,7 @@ const createLiteCheckoutUrl = action( "use server" return json( await withActor(async () => { + await checkCheckoutRateLimit(Actor.account()) const data = await Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method }) await createReferralFromCookie() return { error: undefined, data } diff --git a/packages/console/app/src/routes/workspace/common.tsx b/packages/console/app/src/routes/workspace/common.tsx index d41793dd92b..fb315eefd54 100644 --- a/packages/console/app/src/routes/workspace/common.tsx +++ b/packages/console/app/src/routes/workspace/common.tsx @@ -6,6 +6,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js" import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js" import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" +import { checkCheckoutRateLimit } from "~/routes/zen/util/redis" export function formatDateForTable(date: Date) { const options: Intl.DateTimeFormatOptions = { @@ -77,7 +78,8 @@ export const createCheckoutUrl = action( return json( await withActor( () => - Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl }) + checkCheckoutRateLimit(Actor.account()) + .then(() => Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl })) .then((data) => ({ error: undefined, data })) .catch((e) => ({ error: e.message as string, diff --git a/packages/console/app/src/routes/zen/util/redis.ts b/packages/console/app/src/routes/zen/util/redis.ts index 512523298a8..ef4934bd829 100644 --- a/packages/console/app/src/routes/zen/util/redis.ts +++ b/packages/console/app/src/routes/zen/util/redis.ts @@ -16,3 +16,11 @@ export function getRedis() { export function buildRateLimitKey(kind: string, identifier: string, interval?: string) { return `${Resource.App.stage}:ratelimit:${kind}:${identifier}${interval ? `:${interval}` : ""}` } + +export async function checkCheckoutRateLimit(accountID: string) { + const redis = getRedis() + const key = buildRateLimitKey("checkout", accountID) + const count = await redis.incr(key) + if (count === 1) await redis.expire(key, 60 * 60) + if (count > 5) throw new Error("Too many payment attempts. Please try again later.") +}