fix(billing): add rate limiting, body size check, and logging to checkout endpoint (BUG-079)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 11m9s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 11m9s
- Rate limit /checkout to 3 requests per IP per hour via express-rate-limit - Reject request bodies >1KB (413) - Log checkout session creation with client IP - Bump version to 0.3.4
This commit is contained in:
parent
32a00be0b3
commit
17c1f00e2b
2 changed files with 23 additions and 2 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "docfast-api",
|
"name": "docfast-api",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"description": "Markdown/HTML to PDF API with built-in invoice templates",
|
"description": "Markdown/HTML to PDF API with built-in invoice templates",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Router, Request, Response } from "express";
|
import { Router, Request, Response } from "express";
|
||||||
|
import rateLimit from "express-rate-limit";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { createProKey, downgradeByCustomer, updateEmailByCustomer } from "../services/keys.js";
|
import { createProKey, downgradeByCustomer, updateEmailByCustomer } from "../services/keys.js";
|
||||||
import logger from "../services/logger.js";
|
import logger from "../services/logger.js";
|
||||||
|
|
@ -45,8 +46,25 @@ async function isDocFastSubscription(subscriptionId: string): Promise<boolean> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate limit checkout: max 3 requests per IP per hour
|
||||||
|
const checkoutLimiter = rateLimit({
|
||||||
|
windowMs: 60 * 60 * 1000, // 1 hour
|
||||||
|
max: 3,
|
||||||
|
keyGenerator: (req: Request) => req.ip || req.socket.remoteAddress || "unknown",
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: { error: "Too many checkout requests. Please try again later." },
|
||||||
|
});
|
||||||
|
|
||||||
// Create a Stripe Checkout session for Pro subscription
|
// Create a Stripe Checkout session for Pro subscription
|
||||||
router.post("/checkout", async (_req: Request, res: Response) => {
|
router.post("/checkout", checkoutLimiter, async (req: Request, res: Response) => {
|
||||||
|
// Reject suspiciously large request bodies (>1KB)
|
||||||
|
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
||||||
|
if (contentLength > 1024) {
|
||||||
|
res.status(413).json({ error: "Request body too large" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const priceId = await getOrCreateProPrice();
|
const priceId = await getOrCreateProPrice();
|
||||||
|
|
||||||
|
|
@ -58,6 +76,9 @@ router.post("/checkout", async (_req: Request, res: Response) => {
|
||||||
cancel_url: `${process.env.BASE_URL || "https://docfast.dev"}/#pricing`,
|
cancel_url: `${process.env.BASE_URL || "https://docfast.dev"}/#pricing`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
|
||||||
|
logger.info({ clientIp, sessionId: session.id }, "Checkout session created");
|
||||||
|
|
||||||
res.json({ url: session.url });
|
res.json({ url: session.url });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error({ err }, "Checkout error");
|
logger.error({ err }, "Checkout error");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue