From cf97b6df9cd46a550a1edc7adcb54173761fd707 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 22 Feb 2026 20:45:53 -0800 Subject: [PATCH] Handle billing bad subs, remove exit node name from lock, prevent some stuff on saas --- server/private/lib/lock.ts | 29 ++-- server/setup/copyInConfig.ts | 6 +- server/setup/migrationsPg.ts | 5 + server/setup/migrationsSqlite.ts | 5 + .../settings/(private)/billing/page.tsx | 145 +++++++++++++++--- 5 files changed, 156 insertions(+), 34 deletions(-) diff --git a/server/private/lib/lock.ts b/server/private/lib/lock.ts index 7e68565e..4462a454 100644 --- a/server/private/lib/lock.ts +++ b/server/private/lib/lock.ts @@ -14,6 +14,9 @@ import { config } from "@server/lib/config"; import logger from "@server/logger"; import { redis } from "#private/lib/redis"; +import { v4 as uuidv4 } from "uuid"; + +const instanceId = uuidv4(); export class LockManager { /** @@ -33,7 +36,7 @@ export class LockManager { } const lockValue = `${ - config.getRawConfig().gerbil.exit_node_name + instanceId }:${Date.now()}`; const redisKey = `lock:${lockKey}`; @@ -52,7 +55,7 @@ export class LockManager { if (result === "OK") { logger.debug( `Lock acquired: ${lockKey} by ${ - config.getRawConfig().gerbil.exit_node_name + instanceId }` ); return true; @@ -63,14 +66,14 @@ export class LockManager { if ( existingValue && existingValue.startsWith( - `${config.getRawConfig().gerbil.exit_node_name}:` + `${instanceId}:` ) ) { // Extend the lock TTL since it's the same worker await redis.pexpire(redisKey, ttlMs); logger.debug( `Lock extended: ${lockKey} by ${ - config.getRawConfig().gerbil.exit_node_name + instanceId }` ); return true; @@ -116,7 +119,7 @@ export class LockManager { local key = KEYS[1] local worker_prefix = ARGV[1] local current_value = redis.call('GET', key) - + if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then return redis.call('DEL', key) else @@ -129,19 +132,19 @@ export class LockManager { luaScript, 1, redisKey, - `${config.getRawConfig().gerbil.exit_node_name}:` + `${instanceId}:` )) as number; if (result === 1) { logger.debug( `Lock released: ${lockKey} by ${ - config.getRawConfig().gerbil.exit_node_name + instanceId }` ); } else { logger.warn( `Lock not released - not owned by worker: ${lockKey} by ${ - config.getRawConfig().gerbil.exit_node_name + instanceId }` ); } @@ -198,7 +201,7 @@ export class LockManager { const ownedByMe = exists && value!.startsWith( - `${config.getRawConfig().gerbil.exit_node_name}:` + `${instanceId}:` ); const owner = exists ? value!.split(":")[0] : undefined; @@ -233,7 +236,7 @@ export class LockManager { local worker_prefix = ARGV[1] local ttl = tonumber(ARGV[2]) local current_value = redis.call('GET', key) - + if current_value and string.find(current_value, worker_prefix, 1, true) == 1 then return redis.call('PEXPIRE', key, ttl) else @@ -246,14 +249,14 @@ export class LockManager { luaScript, 1, redisKey, - `${config.getRawConfig().gerbil.exit_node_name}:`, + `${instanceId}:`, ttlMs.toString() )) as number; if (result === 1) { logger.debug( `Lock extended: ${lockKey} by ${ - config.getRawConfig().gerbil.exit_node_name + instanceId } for ${ttlMs}ms` ); return true; @@ -356,7 +359,7 @@ export class LockManager { (value) => value && value.startsWith( - `${config.getRawConfig().gerbil.exit_node_name}:` + `${instanceId}:` ) ).length; } diff --git a/server/setup/copyInConfig.ts b/server/setup/copyInConfig.ts index aa2e040d..be376817 100644 --- a/server/setup/copyInConfig.ts +++ b/server/setup/copyInConfig.ts @@ -2,9 +2,13 @@ import { db, dnsRecords } from "@server/db"; import { domains, exitNodes, orgDomains, orgs, resources } from "@server/db"; import config from "@server/lib/config"; import { eq, ne } from "drizzle-orm"; -import logger from "@server/logger"; +import { build } from "@server/build"; export async function copyInConfig() { + if (build == "saas") { + return; + } + const endpoint = config.getRawConfig().gerbil.base_endpoint; const listenPort = config.getRawConfig().gerbil.start_port; diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index fd28644c..8d27435a 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -19,6 +19,7 @@ import m11 from "./scriptsPg/1.14.0"; import m12 from "./scriptsPg/1.15.0"; import m13 from "./scriptsPg/1.15.3"; import m14 from "./scriptsPg/1.15.4"; +import { build } from "@server/build"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -53,6 +54,10 @@ async function run() { } export async function runMigrations() { + if (build == "saas") { + console.log("Running in SaaS mode, skipping migrations..."); + return; + } if (process.env.DISABLE_MIGRATIONS) { console.log("Migrations are disabled. Skipping..."); return; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 39c133bf..17bc7f19 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -37,6 +37,7 @@ import m32 from "./scriptsSqlite/1.14.0"; import m33 from "./scriptsSqlite/1.15.0"; import m34 from "./scriptsSqlite/1.15.3"; import m35 from "./scriptsSqlite/1.15.4"; +import { build } from "@server/build"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -105,6 +106,10 @@ function backupDb() { } export async function runMigrations() { + if (build == "saas") { + console.log("Running in SaaS mode, skipping migrations..."); + return; + } if (process.env.DISABLE_MIGRATIONS) { console.log("Migrations are disabled. Skipping..."); return; diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index bad8bda2..24fa7480 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -445,6 +445,54 @@ export default function BillingPage() { const currentPlanId = getCurrentPlanId(); + // Check if subscription is in a problematic state that requires attention + const hasProblematicSubscription = (): boolean => { + if (!tierSubscription?.subscription) return false; + const status = tierSubscription.subscription.status; + return ( + status === "past_due" || + status === "unpaid" || + status === "incomplete" || + status === "incomplete_expired" + ); + }; + + const isProblematicState = hasProblematicSubscription(); + + // Get user-friendly subscription status message + const getSubscriptionStatusMessage = (): { title: string; description: string } | null => { + if (!tierSubscription?.subscription || !isProblematicState) return null; + + const status = tierSubscription.subscription.status; + + switch (status) { + case "past_due": + return { + title: t("billingPastDueTitle") || "Payment Past Due", + description: t("billingPastDueDescription") || "Your payment is past due. Please update your payment method to continue using your current plan features. If not resolved, your subscription will be canceled and you'll be reverted to the free tier." + }; + case "unpaid": + return { + title: t("billingUnpaidTitle") || "Subscription Unpaid", + description: t("billingUnpaidDescription") || "Your subscription is unpaid and you have been reverted to the free tier. Please update your payment method to restore your subscription." + }; + case "incomplete": + return { + title: t("billingIncompleteTitle") || "Payment Incomplete", + description: t("billingIncompleteDescription") || "Your payment is incomplete. Please complete the payment process to activate your subscription." + }; + case "incomplete_expired": + return { + title: t("billingIncompleteExpiredTitle") || "Payment Expired", + description: t("billingIncompleteExpiredDescription") || "Your payment was never completed and has expired. You have been reverted to the free tier. Please subscribe again to restore access to paid features." + }; + default: + return null; + } + }; + + const statusMessage = getSubscriptionStatusMessage(); + // Get button label and action for each plan const getPlanAction = (plan: PlanOption) => { if (plan.id === "enterprise") { @@ -458,7 +506,7 @@ export default function BillingPage() { if (plan.id === currentPlanId) { // If it's the basic plan (basic with no subscription), show as current but disabled - if (plan.id === "basic" && !hasSubscription) { + if (plan.id === "basic" && !hasSubscription && !isProblematicState) { return { label: "Current Plan", action: () => {}, @@ -466,8 +514,17 @@ export default function BillingPage() { disabled: true }; } + // If on free tier but has a problematic subscription, allow them to manage it + if (plan.id === "basic" && isProblematicState) { + return { + label: "Manage Subscription", + action: handleModifySubscription, + variant: "default" as const, + disabled: false + }; + } return { - label: "Modify Current Plan", + label: "Manage Current Plan", action: handleModifySubscription, variant: "default" as const, disabled: false @@ -503,7 +560,7 @@ export default function BillingPage() { } }, variant: "outline" as const, - disabled: false + disabled: isProblematicState }; } @@ -522,7 +579,7 @@ export default function BillingPage() { } }, variant: "outline" as const, - disabled: false + disabled: isProblematicState }; }; @@ -648,6 +705,26 @@ export default function BillingPage() { return ( + {/* Subscription Status Alert */} + {isProblematicState && statusMessage && ( + + + + {statusMessage.title} + + + {statusMessage.description} + {" "} + + + + )} + {/* Your Plan Section */} @@ -692,22 +769,50 @@ export default function BillingPage() {
- + {isProblematicState && planAction.disabled && !isCurrentPlan && plan.id !== "enterprise" ? ( + + +
+ +
+
+ +

{t("billingResolvePaymentIssue") || "Please resolve your payment issue before upgrading or downgrading"}

+
+
+ ) : ( + + )}
);