docs: remove free tier, update rate limits and auth for demo+pro model
- Remove free tier from rate limits, add Demo (5/hour, watermarked) - Update auth section: remove free-tier key mention, link to docfast.dev - Update getting started: demo → upgrade to Pro → use API key - Add deprecated: true to /v1/signup/free swagger annotation - Regenerate openapi.json
This commit is contained in:
parent
c35ff2bc97
commit
45b5be248c
8 changed files with 1118 additions and 11 deletions
1
dist/index.js
vendored
1
dist/index.js
vendored
|
|
@ -91,6 +91,7 @@ app.use("/v1/demo", express.json({ limit: "50kb" }), pdfRateLimitMiddleware, dem
|
|||
* /v1/signup/free:
|
||||
* post:
|
||||
* tags: [Account]
|
||||
* deprecated: true
|
||||
* summary: Request a free API key (discontinued)
|
||||
* description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro.
|
||||
* responses:
|
||||
|
|
|
|||
23
dist/routes/billing.js
vendored
23
dist/routes/billing.js
vendored
|
|
@ -1,7 +1,7 @@
|
|||
import { Router } from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import Stripe from "stripe";
|
||||
import { createProKey, downgradeByCustomer, updateEmailByCustomer } from "../services/keys.js";
|
||||
import { createProKey, downgradeByCustomer, updateEmailByCustomer, findKeyByCustomerId } from "../services/keys.js";
|
||||
import logger from "../services/logger.js";
|
||||
function escapeHtml(s) {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
|
|
@ -123,6 +123,27 @@ 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)
|
||||
const existingKey = await findKeyByCustomerId(customerId);
|
||||
if (existingKey) {
|
||||
provisionedSessions.add(session.id);
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html><head><title>DocFast Pro — Key Already Provisioned</title>
|
||||
<style>
|
||||
body { font-family: system-ui; background: #0a0a0a; color: #e8e8e8; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
||||
.card { background: #141414; border: 1px solid #222; border-radius: 16px; padding: 48px; max-width: 500px; text-align: center; }
|
||||
h1 { color: #4f9; margin-bottom: 8px; }
|
||||
p { color: #888; line-height: 1.6; }
|
||||
a { color: #4f9; }
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<h1>✅ Key Already Provisioned</h1>
|
||||
<p>A Pro API key has already been created for this purchase.</p>
|
||||
<p>If you lost your key, use the <a href="/docs#key-recovery">key recovery feature</a>.</p>
|
||||
<p><a href="/docs">View API docs →</a></p>
|
||||
</div></body></html>`);
|
||||
return;
|
||||
}
|
||||
const keyInfo = await createProKey(email, customerId);
|
||||
provisionedSessions.add(session.id);
|
||||
// Return a nice HTML page instead of raw JSON
|
||||
|
|
|
|||
2
dist/services/db.js
vendored
2
dist/services/db.js
vendored
|
|
@ -146,6 +146,8 @@ export async function initDatabase() {
|
|||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_email ON api_keys(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_stripe ON api_keys(stripe_customer_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_stripe_unique
|
||||
ON api_keys(stripe_customer_id) WHERE stripe_customer_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verifications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
|
|
|||
44
dist/services/keys.js
vendored
44
dist/services/keys.js
vendored
|
|
@ -60,21 +60,37 @@ export async function createFreeKey(email) {
|
|||
return entry;
|
||||
}
|
||||
export async function createProKey(email, stripeCustomerId) {
|
||||
// Check in-memory cache first (fast path)
|
||||
const existing = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
if (existing) {
|
||||
existing.tier = "pro";
|
||||
await queryWithRetry("UPDATE api_keys SET tier = 'pro' WHERE key = $1", [existing.key]);
|
||||
return existing;
|
||||
}
|
||||
// UPSERT: handles duplicate webhooks across pods via DB unique index
|
||||
const newKey = generateKey("df_pro");
|
||||
const now = new Date().toISOString();
|
||||
const result = await queryWithRetry(`INSERT INTO api_keys (key, tier, email, created_at, stripe_customer_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (stripe_customer_id) WHERE stripe_customer_id IS NOT NULL
|
||||
DO UPDATE SET tier = 'pro'
|
||||
RETURNING key, tier, email, created_at, stripe_customer_id`, [newKey, "pro", email, now, stripeCustomerId]);
|
||||
const row = result.rows[0];
|
||||
const entry = {
|
||||
key: generateKey("df_pro"),
|
||||
tier: "pro",
|
||||
email,
|
||||
createdAt: new Date().toISOString(),
|
||||
stripeCustomerId,
|
||||
key: row.key,
|
||||
tier: row.tier,
|
||||
email: row.email,
|
||||
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
|
||||
stripeCustomerId: row.stripe_customer_id || undefined,
|
||||
};
|
||||
await queryWithRetry("INSERT INTO api_keys (key, tier, email, created_at, stripe_customer_id) VALUES ($1, $2, $3, $4, $5)", [entry.key, entry.tier, entry.email, entry.createdAt, entry.stripeCustomerId]);
|
||||
keysCache.push(entry);
|
||||
// Refresh in-memory cache
|
||||
const cacheIdx = keysCache.findIndex((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
if (cacheIdx >= 0) {
|
||||
keysCache[cacheIdx] = entry;
|
||||
}
|
||||
else {
|
||||
keysCache.push(entry);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
export async function downgradeByCustomer(stripeCustomerId) {
|
||||
|
|
@ -86,6 +102,20 @@ export async function downgradeByCustomer(stripeCustomerId) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
export async function findKeyByCustomerId(stripeCustomerId) {
|
||||
// Check DB directly — survives pod restarts unlike in-memory cache
|
||||
const result = await queryWithRetry("SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys WHERE stripe_customer_id = $1 LIMIT 1", [stripeCustomerId]);
|
||||
if (result.rows.length === 0)
|
||||
return null;
|
||||
const r = result.rows[0];
|
||||
return {
|
||||
key: r.key,
|
||||
tier: r.tier,
|
||||
email: r.email,
|
||||
createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at,
|
||||
stripeCustomerId: r.stripe_customer_id || undefined,
|
||||
};
|
||||
}
|
||||
export function getAllKeys() {
|
||||
return [...keysCache];
|
||||
}
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "docfast-api",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "docfast-api",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.3",
|
||||
"dependencies": {
|
||||
"compression": "^1.8.1",
|
||||
"express": "^4.21.0",
|
||||
|
|
|
|||
1052
public/openapi.json
Normal file
1052
public/openapi.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -104,6 +104,7 @@ app.use("/v1/demo", express.json({ limit: "50kb" }), pdfRateLimitMiddleware, dem
|
|||
* /v1/signup/free:
|
||||
* post:
|
||||
* tags: [Account]
|
||||
* deprecated: true
|
||||
* summary: Request a free API key (discontinued)
|
||||
* description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro.
|
||||
* responses:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const options: swaggerJsdoc.Options = {
|
|||
title: "DocFast API",
|
||||
version,
|
||||
description:
|
||||
"Convert HTML, Markdown, and URLs to pixel-perfect PDFs. Built-in invoice & receipt templates.\n\n## Authentication\nAll conversion and template endpoints require an API key via `Authorization: Bearer <key>` or `X-API-Key: <key>` header.\n\n## Rate Limits\n- Free tier: 100 PDFs/month, 10 req/min\n- Pro tier: 5,000 PDFs/month, 30 req/min\n\n## Getting Started\n1. Try the demo endpoints (no auth required, 5/hour limit)\n2. Upgrade to Pro at [docfast.dev](https://docfast.dev)\n3. Use your API key to convert documents",
|
||||
"Convert HTML, Markdown, and URLs to pixel-perfect PDFs. Built-in invoice & receipt templates.\n\n## Authentication\nAll conversion endpoints require an API key via `Authorization: Bearer <key>` or `X-API-Key: <key>` header. Get your key at [docfast.dev](https://docfast.dev).\n\n## Rate Limits\n- Demo: 5 conversions/hour, watermarked output\n- Pro tier: 5,000 PDFs/month, 30 req/min\n\n## Getting Started\n1. Try the demo endpoints (no auth required, 5/hour limit)\n2. Upgrade to Pro at [docfast.dev](https://docfast.dev) for clean output and higher limits\n3. Use your API key to convert documents",
|
||||
contact: {
|
||||
name: "DocFast",
|
||||
url: "https://docfast.dev",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue