← back to NationalPaperHangers
Validate the plan tier on billing checkout so installers cannot self-upgrade for free
50ae349256620b5fe94c74343ee3848b2ca9da68 · 2026-05-19 07:59:20 -0700 · Steve
Files touched
Diff
commit 50ae349256620b5fe94c74343ee3848b2ca9da68
Author: Steve <steve@designerwallcoverings.com>
Date: Tue May 19 07:59:20 2026 -0700
Validate the plan tier on billing checkout so installers cannot self-upgrade for free
---
routes/admin.js | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/routes/admin.js b/routes/admin.js
index 873df3d..b8f4024 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -459,9 +459,21 @@ router.get('/billing', async (req, res, next) => {
} catch (err) { next(err); }
});
+// Only these tiers are purchasable. 'basic' is the free default and is never
+// a checkout target. Without this guard the mocked checkout path writes the
+// raw body `tier` straight to installers.tier — a logged-in installer could
+// POST tier=enterprise and self-upgrade for free in mock mode.
+const PURCHASABLE_TIERS = new Set(['pro', 'signature', 'enterprise']);
+const VALID_CADENCES = new Set(['month', 'year']);
+
router.post('/billing/checkout', async (req, res, next) => {
try {
- const { tier, cadence } = req.body;
+ const tier = String(req.body.tier || '').toLowerCase();
+ const cadence = String(req.body.cadence || 'month').toLowerCase();
+ if (!PURCHASABLE_TIERS.has(tier) || !VALID_CADENCES.has(cadence)) {
+ req.session.flash = { error: 'Pick a valid plan to continue.' };
+ return res.redirect('/admin/billing');
+ }
const result = await stripe.createCheckoutSession({
installer: req.installer,
tier, cadence,
← 1e77ada Actually limit verified-brand chips to 3 — LIMIT next to arr
·
back to NationalPaperHangers
·
Constrain profile hero_url to the installer's own uploads, n 258f66a →