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:
|
* /v1/signup/free:
|
||||||
* post:
|
* post:
|
||||||
* tags: [Account]
|
* tags: [Account]
|
||||||
|
* deprecated: true
|
||||||
* summary: Request a free API key (discontinued)
|
* summary: Request a free API key (discontinued)
|
||||||
* description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro.
|
* description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro.
|
||||||
* responses:
|
* responses:
|
||||||
|
|
|
||||||
23
dist/routes/billing.js
vendored
23
dist/routes/billing.js
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import rateLimit from "express-rate-limit";
|
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, findKeyByCustomerId } from "../services/keys.js";
|
||||||
import logger from "../services/logger.js";
|
import logger from "../services/logger.js";
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
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" });
|
res.status(400).json({ error: "No customer found" });
|
||||||
return;
|
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);
|
const keyInfo = await createProKey(email, customerId);
|
||||||
provisionedSessions.add(session.id);
|
provisionedSessions.add(session.id);
|
||||||
// Return a nice HTML page instead of raw JSON
|
// 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_email ON api_keys(email);
|
||||||
CREATE INDEX IF NOT EXISTS idx_api_keys_stripe ON api_keys(stripe_customer_id);
|
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 (
|
CREATE TABLE IF NOT EXISTS verifications (
|
||||||
id SERIAL PRIMARY KEY,
|
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;
|
return entry;
|
||||||
}
|
}
|
||||||
export async function createProKey(email, stripeCustomerId) {
|
export async function createProKey(email, stripeCustomerId) {
|
||||||
|
// Check in-memory cache first (fast path)
|
||||||
const existing = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId);
|
const existing = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.tier = "pro";
|
existing.tier = "pro";
|
||||||
await queryWithRetry("UPDATE api_keys SET tier = 'pro' WHERE key = $1", [existing.key]);
|
await queryWithRetry("UPDATE api_keys SET tier = 'pro' WHERE key = $1", [existing.key]);
|
||||||
return existing;
|
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 = {
|
const entry = {
|
||||||
key: generateKey("df_pro"),
|
key: row.key,
|
||||||
tier: "pro",
|
tier: row.tier,
|
||||||
email,
|
email: row.email,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : row.created_at,
|
||||||
stripeCustomerId,
|
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]);
|
// Refresh in-memory cache
|
||||||
keysCache.push(entry);
|
const cacheIdx = keysCache.findIndex((k) => k.stripeCustomerId === stripeCustomerId);
|
||||||
|
if (cacheIdx >= 0) {
|
||||||
|
keysCache[cacheIdx] = entry;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
keysCache.push(entry);
|
||||||
|
}
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
export async function downgradeByCustomer(stripeCustomerId) {
|
export async function downgradeByCustomer(stripeCustomerId) {
|
||||||
|
|
@ -86,6 +102,20 @@ export async function downgradeByCustomer(stripeCustomerId) {
|
||||||
}
|
}
|
||||||
return false;
|
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() {
|
export function getAllKeys() {
|
||||||
return [...keysCache];
|
return [...keysCache];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "docfast-api",
|
"name": "docfast-api",
|
||||||
"version": "0.4.1",
|
"version": "0.4.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "docfast-api",
|
"name": "docfast-api",
|
||||||
"version": "0.4.1",
|
"version": "0.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
"express": "^4.21.0",
|
"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:
|
* /v1/signup/free:
|
||||||
* post:
|
* post:
|
||||||
* tags: [Account]
|
* tags: [Account]
|
||||||
|
* deprecated: true
|
||||||
* summary: Request a free API key (discontinued)
|
* summary: Request a free API key (discontinued)
|
||||||
* description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro.
|
* description: Free accounts have been discontinued. Use the demo endpoints or upgrade to Pro.
|
||||||
* responses:
|
* responses:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const options: swaggerJsdoc.Options = {
|
||||||
title: "DocFast API",
|
title: "DocFast API",
|
||||||
version,
|
version,
|
||||||
description:
|
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: {
|
contact: {
|
||||||
name: "DocFast",
|
name: "DocFast",
|
||||||
url: "https://docfast.dev",
|
url: "https://docfast.dev",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue