fix: OpenAPI spec accuracy — hide internal endpoints, mark signup/verify deprecated
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m9s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m9s
- Remove @openapi annotations from /v1/billing/webhook (Stripe-internal) - Remove @openapi annotations from /v1/billing/success (browser redirect) - Mark /v1/signup/verify as deprecated (returns 410) - Add 3 TDD tests in openapi-spec.test.ts - Update 2 existing tests in app-routes.test.ts - 530 tests passing (was 527)
This commit is contained in:
parent
1d5d9adf08
commit
6b1b3d584e
15 changed files with 399 additions and 290 deletions
110
dist/routes/billing.js
vendored
110
dist/routes/billing.js
vendored
|
|
@ -15,8 +15,29 @@ function getStripe() {
|
|||
return _stripe;
|
||||
}
|
||||
const router = Router();
|
||||
// Track provisioned session IDs to prevent duplicate key creation
|
||||
const provisionedSessions = new Set();
|
||||
// Track provisioned session IDs with TTL to prevent duplicate key creation and memory leaks
|
||||
// Map<sessionId, timestamp> - entries older than 24h are periodically cleaned up
|
||||
const provisionedSessions = new Map();
|
||||
// TTL Configuration
|
||||
const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // Clean up every 1 hour
|
||||
// Cleanup old provisioned session entries
|
||||
function cleanupOldSessions() {
|
||||
const now = Date.now();
|
||||
const cutoff = now - SESSION_TTL_MS;
|
||||
let cleanedCount = 0;
|
||||
for (const [sessionId, timestamp] of provisionedSessions.entries()) {
|
||||
if (timestamp < cutoff) {
|
||||
provisionedSessions.delete(sessionId);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
if (cleanedCount > 0) {
|
||||
logger.info({ cleanedCount, remainingCount: provisionedSessions.size }, "Cleaned up expired provisioned sessions");
|
||||
}
|
||||
}
|
||||
// Start periodic cleanup
|
||||
setInterval(cleanupOldSessions, CLEANUP_INTERVAL_MS);
|
||||
const DOCFAST_PRODUCT_ID = "prod_TygeG8tQPtEAdE";
|
||||
// Returns true if the given Stripe subscription contains a DocFast product.
|
||||
// Used to filter webhook events — this Stripe account is shared with other projects.
|
||||
|
|
@ -101,43 +122,15 @@ router.post("/checkout", checkoutLimiter, async (req, res) => {
|
|||
res.status(500).json({ error: "Failed to create checkout session" });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/billing/success:
|
||||
* get:
|
||||
* tags: [Billing]
|
||||
* summary: Checkout success page
|
||||
* description: |
|
||||
* Provisions a Pro API key after successful Stripe checkout and displays it in an HTML page.
|
||||
* Called by Stripe redirect after payment completion.
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: session_id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Stripe Checkout session ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: HTML page displaying the new API key
|
||||
* content:
|
||||
* text/html:
|
||||
* schema:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: Missing session_id or no customer found
|
||||
* 409:
|
||||
* description: Checkout session already used
|
||||
* 500:
|
||||
* description: Failed to retrieve session
|
||||
*/
|
||||
// Success page — provision Pro API key after checkout
|
||||
// Success page — provision Pro API key after checkout (browser redirect, not a public API)
|
||||
router.get("/success", async (req, res) => {
|
||||
const sessionId = req.query.session_id;
|
||||
if (!sessionId) {
|
||||
res.status(400).json({ error: "Missing session_id" });
|
||||
return;
|
||||
}
|
||||
// Clean up old sessions before checking duplicates
|
||||
cleanupOldSessions();
|
||||
// Prevent duplicate provisioning from same session
|
||||
if (provisionedSessions.has(sessionId)) {
|
||||
res.status(409).json({ error: "This checkout session has already been used to provision a key. If you lost your key, use the key recovery feature." });
|
||||
|
|
@ -151,10 +144,10 @@ router.get("/success", async (req, res) => {
|
|||
res.status(400).json({ error: "No customer found" });
|
||||
return;
|
||||
}
|
||||
// Check DB for existing key (survives pod restarts, unlike provisionedSessions Set)
|
||||
// Check DB for existing key (survives pod restarts, unlike provisionedSessions Map)
|
||||
const existingKey = await findKeyByCustomerId(customerId);
|
||||
if (existingKey) {
|
||||
provisionedSessions.add(session.id);
|
||||
provisionedSessions.set(session.id, Date.now());
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html><head><title>DocFast Pro — Key Already Provisioned</title>
|
||||
<style>
|
||||
|
|
@ -173,7 +166,7 @@ a { color: #4f9; }
|
|||
return;
|
||||
}
|
||||
const keyInfo = await createProKey(email, customerId);
|
||||
provisionedSessions.add(session.id);
|
||||
provisionedSessions.set(session.id, Date.now());
|
||||
// Return a nice HTML page instead of raw JSON
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html><head><title>Welcome to DocFast Pro!</title>
|
||||
|
|
@ -202,48 +195,7 @@ a { color: #4f9; }
|
|||
res.status(500).json({ error: "Failed to retrieve session" });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/billing/webhook:
|
||||
* post:
|
||||
* tags: [Billing]
|
||||
* summary: Stripe webhook endpoint
|
||||
* description: |
|
||||
* Receives Stripe webhook events for subscription lifecycle management.
|
||||
* Requires the raw request body and a valid Stripe-Signature header for verification.
|
||||
* Handles checkout.session.completed, customer.subscription.updated,
|
||||
* customer.subscription.deleted, and customer.updated events.
|
||||
* parameters:
|
||||
* - in: header
|
||||
* name: Stripe-Signature
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: Stripe webhook signature for payload verification
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* description: Raw Stripe event payload
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Webhook received
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* received:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* 400:
|
||||
* description: Missing Stripe-Signature header or invalid signature
|
||||
* 500:
|
||||
* description: Webhook secret not configured
|
||||
*/
|
||||
// Stripe webhook for subscription lifecycle events
|
||||
// Stripe webhook for subscription lifecycle events (internal, not in public API docs)
|
||||
router.post("/webhook", async (req, res) => {
|
||||
const sig = req.headers["stripe-signature"];
|
||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
||||
|
|
@ -297,7 +249,7 @@ router.post("/webhook", async (req, res) => {
|
|||
break;
|
||||
}
|
||||
const keyInfo = await createProKey(email, customerId);
|
||||
provisionedSessions.add(session.id);
|
||||
provisionedSessions.set(session.id, Date.now());
|
||||
logger.info({ email, customerId }, "checkout.session.completed: provisioned pro key");
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue