Backend hardening: structured logging, timeouts, memory leak fixes, compression, XSS fix
Some checks failed
Deploy to Production / Deploy to Server (push) Failing after 20s
Some checks failed
Deploy to Production / Deploy to Server (push) Failing after 20s
- Add pino structured logging with request IDs (X-Request-Id header) - Add 30s timeout to acquirePage() and renderPdf/renderUrlPdf - Add verification cache cleanup (every 15min) and rate limit cleanup (every 60s) - Read version from package.json in health endpoint - Add compression middleware - Escape currency in templates (XSS fix) - Add static asset caching (1h maxAge) - Remove deprecated docker-compose version field - Replace all console.log/error with pino logger
This commit is contained in:
parent
4833edf44c
commit
9541ae1826
20 changed files with 319 additions and 74 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { Router, Request, Response } from "express";
|
||||
import Stripe from "stripe";
|
||||
import { createProKey, revokeByCustomer } from "../services/keys.js";
|
||||
import logger from "../services/logger.js";
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
|
|
@ -33,7 +34,7 @@ router.post("/checkout", async (_req: Request, res: Response) => {
|
|||
|
||||
res.json({ url: session.url });
|
||||
} catch (err: any) {
|
||||
console.error("Checkout error:", err.message);
|
||||
logger.error({ err }, "Checkout error");
|
||||
res.status(500).json({ error: "Failed to create checkout session" });
|
||||
}
|
||||
});
|
||||
|
|
@ -79,7 +80,7 @@ a { color: #4f9; }
|
|||
<p><a href="/docs">View API docs →</a></p>
|
||||
</div></body></html>`);
|
||||
} catch (err: any) {
|
||||
console.error("Success page error:", err.message);
|
||||
logger.error({ err }, "Success page error");
|
||||
res.status(500).json({ error: "Failed to retrieve session" });
|
||||
}
|
||||
});
|
||||
|
|
@ -97,7 +98,7 @@ router.post("/webhook", async (req: Request, res: Response) => {
|
|||
try {
|
||||
event = JSON.parse(typeof req.body === "string" ? req.body : req.body.toString()) as Stripe.Event;
|
||||
} catch (err: any) {
|
||||
console.error("Failed to parse webhook body:", err.message);
|
||||
logger.error({ err }, "Failed to parse webhook body");
|
||||
res.status(400).json({ error: "Invalid payload" });
|
||||
return;
|
||||
}
|
||||
|
|
@ -108,7 +109,7 @@ router.post("/webhook", async (req: Request, res: Response) => {
|
|||
try {
|
||||
event = getStripe().webhooks.constructEvent(req.body, sig, webhookSecret);
|
||||
} catch (err: any) {
|
||||
console.error("Webhook signature verification failed:", err.message);
|
||||
logger.error({ err }, "Webhook signature verification failed");
|
||||
res.status(400).json({ error: "Invalid signature" });
|
||||
return;
|
||||
}
|
||||
|
|
@ -133,11 +134,11 @@ router.post("/webhook", async (req: Request, res: Response) => {
|
|||
return productId === DOCFAST_PRODUCT_ID;
|
||||
});
|
||||
if (!hasDocfastProduct) {
|
||||
console.log(`Ignoring event for different product (session: ${session.id})`);
|
||||
logger.info({ sessionId: session.id }, "Ignoring event for different product");
|
||||
break;
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`Failed to retrieve session line_items: ${err.message}, skipping`);
|
||||
logger.error({ err, sessionId: session.id }, "Failed to retrieve session line_items");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -147,14 +148,14 @@ router.post("/webhook", async (req: Request, res: Response) => {
|
|||
}
|
||||
|
||||
const keyInfo = await createProKey(email, customerId);
|
||||
console.log(`checkout.session.completed: provisioned pro key for ${email} (customer: ${customerId}, key: ${keyInfo.key.slice(0, 12)}...)`);
|
||||
logger.info({ email, customerId }, "checkout.session.completed: provisioned pro key");
|
||||
break;
|
||||
}
|
||||
case "customer.subscription.deleted": {
|
||||
const sub = event.data.object as Stripe.Subscription;
|
||||
const customerId = sub.customer as string;
|
||||
await revokeByCustomer(customerId);
|
||||
console.log(`Subscription cancelled for ${customerId}, key revoked`);
|
||||
logger.info({ customerId }, "Subscription cancelled, key revoked");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Router, Request, Response } from "express";
|
|||
import { renderPdf, renderUrlPdf, getPoolStats } from "../services/browser.js";
|
||||
import { markdownToHtml, wrapHtml } from "../services/markdown.js";
|
||||
import dns from "node:dns/promises";
|
||||
import logger from "../services/logger.js";
|
||||
import net from "node:net";
|
||||
|
||||
function isPrivateIP(ip: string): boolean {
|
||||
|
|
@ -74,7 +75,7 @@ convertRouter.post("/html", async (req: Request & { acquirePdfSlot?: () => Promi
|
|||
res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
|
||||
res.send(pdf);
|
||||
} catch (err: any) {
|
||||
console.error("Convert HTML error:", err);
|
||||
logger.error({ err }, "Convert HTML error");
|
||||
if (err.message === "QUEUE_FULL") {
|
||||
res.status(429).json({ error: "Server busy - too many concurrent PDF generations. Please try again in a few seconds." });
|
||||
return;
|
||||
|
|
@ -118,7 +119,7 @@ convertRouter.post("/markdown", async (req: Request & { acquirePdfSlot?: () => P
|
|||
res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
|
||||
res.send(pdf);
|
||||
} catch (err: any) {
|
||||
console.error("Convert MD error:", err);
|
||||
logger.error({ err }, "Convert MD error");
|
||||
if (err.message === "QUEUE_FULL") {
|
||||
res.status(429).json({ error: "Server busy - too many concurrent PDF generations. Please try again in a few seconds." });
|
||||
return;
|
||||
|
|
@ -186,7 +187,7 @@ convertRouter.post("/url", async (req: Request & { acquirePdfSlot?: () => Promis
|
|||
res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
|
||||
res.send(pdf);
|
||||
} catch (err: any) {
|
||||
console.error("Convert URL error:", err);
|
||||
logger.error({ err }, "Convert URL error");
|
||||
if (err.message === "QUEUE_FULL") {
|
||||
res.status(429).json({ error: "Server busy - too many concurrent PDF generations. Please try again in a few seconds." });
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import rateLimit from "express-rate-limit";
|
|||
import { createPendingVerification, verifyCode } from "../services/verification.js";
|
||||
import { sendVerificationEmail } from "../services/email.js";
|
||||
import { getAllKeys, updateKeyEmail } from "../services/keys.js";
|
||||
import logger from "../services/logger.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ router.post("/", changeLimiter, async (req: Request, res: Response) => {
|
|||
const pending = await createPendingVerification(cleanEmail);
|
||||
|
||||
sendVerificationEmail(cleanEmail, (pending as any).code).catch((err: Error) => {
|
||||
console.error(`Failed to send email change verification to ${cleanEmail}:`, err);
|
||||
logger.error({ err, email: cleanEmail }, "Failed to send email change verification");
|
||||
});
|
||||
|
||||
res.json({ status: "verification_sent", message: "Verification code sent to your new email address." });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { Router } from "express";
|
||||
import { createRequire } from "module";
|
||||
import { getPoolStats } from "../services/browser.js";
|
||||
import { pool } from "../services/db.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version: APP_VERSION } = require("../../package.json");
|
||||
|
||||
export const healthRouter = Router();
|
||||
|
||||
healthRouter.get("/", async (_req, res) => {
|
||||
|
|
@ -38,7 +42,7 @@ healthRouter.get("/", async (_req, res) => {
|
|||
|
||||
const response = {
|
||||
status: overallStatus,
|
||||
version: "0.2.1",
|
||||
version: APP_VERSION,
|
||||
database: databaseStatus,
|
||||
pool: {
|
||||
size: poolStats.poolSize,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import rateLimit from "express-rate-limit";
|
|||
import { createPendingVerification, verifyCode } from "../services/verification.js";
|
||||
import { sendVerificationEmail } from "../services/email.js";
|
||||
import { getAllKeys } from "../services/keys.js";
|
||||
import logger from "../services/logger.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ router.post("/", recoverLimiter, async (req: Request, res: Response) => {
|
|||
const pending = await createPendingVerification(cleanEmail);
|
||||
|
||||
sendVerificationEmail(cleanEmail, pending.code).catch(err => {
|
||||
console.error(`Failed to send recovery email to ${cleanEmail}:`, err);
|
||||
logger.error({ err, email: cleanEmail }, "Failed to send recovery email");
|
||||
});
|
||||
|
||||
res.json({ status: "recovery_sent", message: "If an account exists for this email, a verification code has been sent." });
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import rateLimit from "express-rate-limit";
|
|||
import { createFreeKey } from "../services/keys.js";
|
||||
import { createVerification, createPendingVerification, verifyCode, isEmailVerified, getVerifiedApiKey } from "../services/verification.js";
|
||||
import { sendVerificationEmail } from "../services/email.js";
|
||||
import logger from "../services/logger.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ router.post("/free", rejectDuplicateEmail, signupLimiter, async (req: Request, r
|
|||
const pending = await createPendingVerification(cleanEmail);
|
||||
|
||||
sendVerificationEmail(cleanEmail, pending.code).catch(err => {
|
||||
console.error(`Failed to send verification email to ${cleanEmail}:`, err);
|
||||
logger.error({ err, email: cleanEmail }, "Failed to send verification email");
|
||||
});
|
||||
|
||||
res.json({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Router, Request, Response } from "express";
|
||||
import { renderPdf } from "../services/browser.js";
|
||||
import logger from "../services/logger.js";
|
||||
import { templates, renderTemplate } from "../services/templates.js";
|
||||
|
||||
export const templatesRouter = Router();
|
||||
|
|
@ -37,7 +38,7 @@ templatesRouter.post("/:id/render", async (req: Request, res: Response) => {
|
|||
res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
|
||||
res.send(pdf);
|
||||
} catch (err: any) {
|
||||
console.error("Template render error:", err);
|
||||
logger.error({ err }, "Template render error");
|
||||
res.status(500).json({ error: "Template rendering failed", detail: err.message });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue