Skip to content

Commit f2eba8d

Browse files
committed
fix(affiliate): validate commission inputs
1 parent c30ca23 commit f2eba8d

3 files changed

Lines changed: 34 additions & 1 deletion

File tree

app/api/affiliate/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { readSession, authConfigured, SESSION_COOKIE } from "@/lib/session";
33
import { getAffiliate, enrollAffiliate, setAffiliateCommission, listReferrals, AFFILIATE_FLOOR, findOrCreateAccountByEmail } from "@/lib/db";
4+
import { parseCommissionPercent } from "@/lib/affiliate";
45

56
export const runtime = "nodejs";
67
export const dynamic = "force-dynamic";
@@ -54,7 +55,9 @@ export async function POST(req: NextRequest) {
5455
if (aff.plan !== "paid") {
5556
return NextResponse.json({ error: `Free plan is floored at ${AFFILIATE_FLOOR}%. Upgrade to $1/mo to lower it.` }, { status: 403 });
5657
}
57-
await setAffiliateCommission(id, Number(body.commission_pct));
58+
const pct = parseCommissionPercent(body.commission_pct);
59+
if (pct == null) return NextResponse.json({ error: "Enter a valid commission percentage." }, { status: 400 });
60+
await setAffiliateCommission(id, pct);
5861
} else {
5962
// Default action: enroll (idempotent).
6063
await enrollAffiliate(id);

lib/affiliate.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const COMMISSION_RE = /^(?:\d+)(?:\.\d{1,2})?$/;
2+
3+
export function parseCommissionPercent(value: unknown): number | null {
4+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
5+
if (typeof value !== "string") return null;
6+
const text = value.trim();
7+
if (!COMMISSION_RE.test(text)) return null;
8+
const pct = Number(text);
9+
return Number.isFinite(pct) ? pct : null;
10+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import { parseCommissionPercent } from "../lib/affiliate.ts";
5+
6+
test("parseCommissionPercent accepts finite numbers and decimal strings", () => {
7+
assert.equal(parseCommissionPercent(25), 25);
8+
assert.equal(parseCommissionPercent("80"), 80);
9+
assert.equal(parseCommissionPercent(" 12.5 "), 12.5);
10+
});
11+
12+
test("parseCommissionPercent rejects coerced or malformed values", () => {
13+
assert.equal(parseCommissionPercent("1e2"), null);
14+
assert.equal(parseCommissionPercent("0x10"), null);
15+
assert.equal(parseCommissionPercent("80abc"), null);
16+
assert.equal(parseCommissionPercent(["80"]), null);
17+
assert.equal(parseCommissionPercent({ valueOf: () => 80 }), null);
18+
assert.equal(parseCommissionPercent(Number.NaN), null);
19+
assert.equal(parseCommissionPercent(Number.POSITIVE_INFINITY), null);
20+
});

0 commit comments

Comments
 (0)