Fix audit #14 (body size limits), #17 (duplicate session_id), #22 (unused import)
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 2m53s
All checks were successful
Deploy to Production / Deploy to Server (push) Successful in 2m53s
This commit is contained in:
parent
6cc30db5c6
commit
09c6feb06e
9 changed files with 36 additions and 10 deletions
5
dist/index.js
vendored
5
dist/index.js
vendored
|
|
@ -86,8 +86,9 @@ app.use("/v1/signup", signupRouter);
|
||||||
app.use("/v1/recover", recoverRouter);
|
app.use("/v1/recover", recoverRouter);
|
||||||
app.use("/v1/billing", billingRouter);
|
app.use("/v1/billing", billingRouter);
|
||||||
app.use("/v1/email-change", emailChangeRouter);
|
app.use("/v1/email-change", emailChangeRouter);
|
||||||
// Authenticated routes
|
// Authenticated routes — conversion routes get tighter body limits (500KB)
|
||||||
app.use("/v1/convert", authMiddleware, usageMiddleware, pdfRateLimitMiddleware, convertRouter);
|
const convertBodyLimit = express.json({ limit: "500kb" });
|
||||||
|
app.use("/v1/convert", convertBodyLimit, authMiddleware, usageMiddleware, pdfRateLimitMiddleware, convertRouter);
|
||||||
app.use("/v1/templates", authMiddleware, usageMiddleware, templatesRouter);
|
app.use("/v1/templates", authMiddleware, usageMiddleware, templatesRouter);
|
||||||
// Admin: usage stats (admin key required)
|
// Admin: usage stats (admin key required)
|
||||||
const adminAuth = (req, res, next) => {
|
const adminAuth = (req, res, next) => {
|
||||||
|
|
|
||||||
9
dist/routes/billing.js
vendored
9
dist/routes/billing.js
vendored
|
|
@ -16,6 +16,8 @@ function getStripe() {
|
||||||
return _stripe;
|
return _stripe;
|
||||||
}
|
}
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
// Track provisioned session IDs to prevent duplicate key creation
|
||||||
|
const provisionedSessions = new Set();
|
||||||
// Create a Stripe Checkout session for Pro subscription
|
// Create a Stripe Checkout session for Pro subscription
|
||||||
router.post("/checkout", async (_req, res) => {
|
router.post("/checkout", async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -41,6 +43,11 @@ router.get("/success", async (req, res) => {
|
||||||
res.status(400).json({ error: "Missing session_id" });
|
res.status(400).json({ error: "Missing session_id" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Prevent duplicate provisioning from same session
|
||||||
|
if (provisionedSessions.has(sessionId)) {
|
||||||
|
res.status(409).send("This checkout session has already been used to provision a key. If you lost your key, use the key recovery feature.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const session = await getStripe().checkout.sessions.retrieve(sessionId);
|
const session = await getStripe().checkout.sessions.retrieve(sessionId);
|
||||||
const customerId = session.customer;
|
const customerId = session.customer;
|
||||||
|
|
@ -50,6 +57,7 @@ router.get("/success", async (req, res) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keyInfo = await createProKey(email, customerId);
|
const keyInfo = await createProKey(email, customerId);
|
||||||
|
provisionedSessions.add(session.id);
|
||||||
// Return a nice HTML page instead of raw JSON
|
// Return a nice HTML page instead of raw JSON
|
||||||
res.send(`<!DOCTYPE html>
|
res.send(`<!DOCTYPE html>
|
||||||
<html><head><title>Welcome to DocFast Pro!</title>
|
<html><head><title>Welcome to DocFast Pro!</title>
|
||||||
|
|
@ -131,6 +139,7 @@ router.post("/webhook", async (req, res) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const keyInfo = await createProKey(email, customerId);
|
const keyInfo = await createProKey(email, customerId);
|
||||||
|
provisionedSessions.add(session.id);
|
||||||
logger.info({ email, customerId }, "checkout.session.completed: provisioned pro key");
|
logger.info({ email, customerId }, "checkout.session.completed: provisioned pro key");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,9 @@ footer .container { display: flex; justify-content: space-between; align-items:
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/status">API Status</a>
|
<a href="/health">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
|
<a href="mailto:support@docfast.dev">Support</a>
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms">Terms of Service</a>
|
||||||
|
|
|
||||||
|
|
@ -421,8 +421,9 @@ html, body {
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/status">API Status</a>
|
<a href="/health">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
|
<a href="mailto:support@docfast.dev">Support</a>
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms">Terms of Service</a>
|
||||||
|
|
|
||||||
|
|
@ -178,8 +178,9 @@ footer .container { display: flex; justify-content: space-between; align-items:
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/status">API Status</a>
|
<a href="/health">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
|
<a href="mailto:support@docfast.dev">Support</a>
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms">Terms of Service</a>
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ footer .container { display: flex; justify-content: space-between; align-items:
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Target:</strong> 99.5% uptime (best effort, no SLA for free tier)</li>
|
<li><strong>Target:</strong> 99.5% uptime (best effort, no SLA for free tier)</li>
|
||||||
<li><strong>Maintenance:</strong> Scheduled maintenance with advance notice</li>
|
<li><strong>Maintenance:</strong> Scheduled maintenance with advance notice</li>
|
||||||
<li><strong>Status page:</strong> <a href="/status">https://docfast.dev/status</a></li>
|
<li><strong>Status page:</strong> <a href="/health">https://docfast.dev/health</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>5.2 Performance</h3>
|
<h3>5.2 Performance</h3>
|
||||||
|
|
@ -250,8 +250,9 @@ footer .container { display: flex; justify-content: space-between; align-items:
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/docs">Docs</a>
|
<a href="/docs">Docs</a>
|
||||||
<a href="/status">API Status</a>
|
<a href="/health">API Status</a>
|
||||||
<a href="/#change-email" class="open-email-change">Change Email</a>
|
<a href="/#change-email" class="open-email-change">Change Email</a>
|
||||||
|
<a href="mailto:support@docfast.dev">Support</a>
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms">Terms of Service</a>
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,9 @@ app.use("/v1/recover", recoverRouter);
|
||||||
app.use("/v1/billing", billingRouter);
|
app.use("/v1/billing", billingRouter);
|
||||||
app.use("/v1/email-change", emailChangeRouter);
|
app.use("/v1/email-change", emailChangeRouter);
|
||||||
|
|
||||||
// Authenticated routes
|
// Authenticated routes — conversion routes get tighter body limits (500KB)
|
||||||
app.use("/v1/convert", authMiddleware, usageMiddleware, pdfRateLimitMiddleware, convertRouter);
|
const convertBodyLimit = express.json({ limit: "500kb" });
|
||||||
|
app.use("/v1/convert", convertBodyLimit, authMiddleware, usageMiddleware, pdfRateLimitMiddleware, convertRouter);
|
||||||
app.use("/v1/templates", authMiddleware, usageMiddleware, templatesRouter);
|
app.use("/v1/templates", authMiddleware, usageMiddleware, templatesRouter);
|
||||||
|
|
||||||
// Admin: usage stats (admin key required)
|
// Admin: usage stats (admin key required)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ function getStripe(): Stripe {
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Track provisioned session IDs to prevent duplicate key creation
|
||||||
|
const provisionedSessions = new Set<string>();
|
||||||
|
|
||||||
// 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", async (_req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -47,6 +50,12 @@ router.get("/success", async (req: Request, res: Response) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate provisioning from same session
|
||||||
|
if (provisionedSessions.has(sessionId)) {
|
||||||
|
res.status(409).send("This checkout session has already been used to provision a key. If you lost your key, use the key recovery feature.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await getStripe().checkout.sessions.retrieve(sessionId);
|
const session = await getStripe().checkout.sessions.retrieve(sessionId);
|
||||||
const customerId = session.customer as string;
|
const customerId = session.customer as string;
|
||||||
|
|
@ -58,6 +67,7 @@ router.get("/success", async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyInfo = await createProKey(email, customerId);
|
const keyInfo = await createProKey(email, customerId);
|
||||||
|
provisionedSessions.add(session.id);
|
||||||
|
|
||||||
// Return a nice HTML page instead of raw JSON
|
// Return a nice HTML page instead of raw JSON
|
||||||
res.send(`<!DOCTYPE html>
|
res.send(`<!DOCTYPE html>
|
||||||
|
|
@ -142,6 +152,7 @@ router.post("/webhook", async (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyInfo = await createProKey(email, customerId);
|
const keyInfo = await createProKey(email, customerId);
|
||||||
|
provisionedSessions.add(session.id);
|
||||||
logger.info({ email, customerId }, "checkout.session.completed: provisioned pro key");
|
logger.info({ email, customerId }, "checkout.session.completed: provisioned pro key");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Router, Request, Response } from "express";
|
import { Router, Request, Response } from "express";
|
||||||
import { renderPdf, renderUrlPdf, getPoolStats } from "../services/browser.js";
|
import { renderPdf, renderUrlPdf } from "../services/browser.js";
|
||||||
import { markdownToHtml, wrapHtml } from "../services/markdown.js";
|
import { markdownToHtml, wrapHtml } from "../services/markdown.js";
|
||||||
import dns from "node:dns/promises";
|
import dns from "node:dns/promises";
|
||||||
import logger from "../services/logger.js";
|
import logger from "../services/logger.js";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue