From 45b5be248cbedbb0af3863a61c3c657861401843 Mon Sep 17 00:00:00 2001 From: DocFast Bot Date: Fri, 20 Feb 2026 19:10:25 +0000 Subject: [PATCH] docs: remove free tier, update rate limits and auth for demo+pro model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- dist/index.js | 1 + dist/routes/billing.js | 23 +- dist/services/db.js | 2 + dist/services/keys.js | 44 +- package-lock.json | 4 +- public/openapi.json | 1052 ++++++++++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/swagger.ts | 2 +- 8 files changed, 1118 insertions(+), 11 deletions(-) create mode 100644 public/openapi.json diff --git a/dist/index.js b/dist/index.js index dbcf484..a758c6f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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: diff --git a/dist/routes/billing.js b/dist/routes/billing.js index dbc7c3b..761fda1 100644 --- a/dist/routes/billing.js +++ b/dist/routes/billing.js @@ -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, "'"); @@ -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(` +DocFast Pro — Key Already Provisioned + +
+

✅ Key Already Provisioned

+

A Pro API key has already been created for this purchase.

+

If you lost your key, use the key recovery feature.

+

View API docs →

+
`); + return; + } const keyInfo = await createProKey(email, customerId); provisionedSessions.add(session.id); // Return a nice HTML page instead of raw JSON diff --git a/dist/services/db.js b/dist/services/db.js index 1814736..35af8bb 100644 --- a/dist/services/db.js +++ b/dist/services/db.js @@ -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, diff --git a/dist/services/keys.js b/dist/services/keys.js index 88ad4a4..4873704 100644 --- a/dist/services/keys.js +++ b/dist/services/keys.js @@ -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]; } diff --git a/package-lock.json b/package-lock.json index c80e975..6914021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/public/openapi.json b/public/openapi.json new file mode 100644 index 0000000..2192f5d --- /dev/null +++ b/public/openapi.json @@ -0,0 +1,1052 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "DocFast API", + "version": "1.0.0", + "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 ` or `X-API-Key: ` header.\n\n## Demo Endpoints\nTry the API without signing up! Demo endpoints are public (no API key needed) but rate-limited to 5 requests/hour per IP and produce watermarked PDFs.\n\n## Rate Limits\n- Demo: 5 PDFs/hour per IP (watermarked)\n- Pro tier: 5,000 PDFs/month, 30 req/min\n\n## Getting Started\n1. Try the demo at `POST /v1/demo/html` — no signup needed\n2. Subscribe to Pro at [docfast.dev](https://docfast.dev/#pricing) for clean PDFs\n3. Use your API key to convert documents", + "contact": { + "name": "DocFast", + "url": "https://docfast.dev", + "email": "support@docfast.dev" + } + }, + "servers": [ + { + "url": "https://docfast.dev", + "description": "Production" + } + ], + "tags": [ + { + "name": "Demo", + "description": "Try the API without signing up — watermarked PDFs, rate-limited" + }, + { + "name": "Conversion", + "description": "Convert HTML, Markdown, or URLs to PDF (requires API key)" + }, + { + "name": "Templates", + "description": "Built-in document templates" + }, + { + "name": "Account", + "description": "Key recovery and email management" + }, + { + "name": "Billing", + "description": "Stripe-powered subscription management" + }, + { + "name": "System", + "description": "Health checks and usage stats" + } + ], + "components": { + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "API key as Bearer token" + }, + "ApiKeyHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key", + "description": "API key via X-API-Key header" + } + }, + "schemas": { + "PdfOptions": { + "type": "object", + "properties": { + "format": { + "type": "string", + "enum": [ + "A4", + "Letter", + "Legal", + "A3", + "A5", + "Tabloid" + ], + "default": "A4", + "description": "Page size" + }, + "landscape": { + "type": "boolean", + "default": false, + "description": "Landscape orientation" + }, + "margin": { + "type": "object", + "properties": { + "top": { + "type": "string", + "description": "Top margin (e.g. \"10mm\", \"1in\")", + "default": "0" + }, + "right": { + "type": "string", + "description": "Right margin", + "default": "0" + }, + "bottom": { + "type": "string", + "description": "Bottom margin", + "default": "0" + }, + "left": { + "type": "string", + "description": "Left margin", + "default": "0" + } + }, + "description": "Page margins" + }, + "printBackground": { + "type": "boolean", + "default": true, + "description": "Print background colors and images" + }, + "filename": { + "type": "string", + "description": "Custom filename for Content-Disposition header", + "default": "document.pdf" + } + } + }, + "Error": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message" + } + }, + "required": [ + "error" + ] + } + } + }, + "paths": { + "/v1/billing/checkout": { + "post": { + "tags": [ + "Billing" + ], + "summary": "Create a Stripe checkout session", + "description": "Creates a Stripe Checkout session for a Pro subscription (€9/month).\nReturns a URL to redirect the user to Stripe's hosted payment page.\nRate limited to 3 requests per hour per IP.\n", + "responses": { + "200": { + "description": "Checkout session created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "Stripe Checkout URL to redirect the user to" + } + } + } + } + } + }, + "413": { + "description": "Request body too large" + }, + "429": { + "description": "Too many checkout requests" + }, + "500": { + "description": "Failed to create checkout session" + } + } + } + }, + "/v1/convert/html": { + "post": { + "tags": [ + "Conversion" + ], + "summary": "Convert HTML to PDF", + "description": "Converts HTML content to a PDF document. Bare HTML fragments are automatically wrapped in a full HTML document.", + "security": [ + { + "BearerAuth": [] + }, + { + "ApiKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "required": [ + "html" + ], + "properties": { + "html": { + "type": "string", + "description": "HTML content to convert. Can be a full document or a fragment.", + "example": "

Hello World

My first PDF

" + }, + "css": { + "type": "string", + "description": "Optional CSS to inject (only used when html is a fragment, not a full document)", + "example": "body { font-family: sans-serif; padding: 40px; }" + } + } + }, + { + "$ref": "#/components/schemas/PdfOptions" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "PDF document", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Missing html field" + }, + "401": { + "description": "Missing API key" + }, + "403": { + "description": "Invalid API key" + }, + "415": { + "description": "Unsupported Content-Type (must be application/json)" + }, + "429": { + "description": "Rate limit or usage limit exceeded" + }, + "500": { + "description": "PDF generation failed" + } + } + } + }, + "/v1/convert/markdown": { + "post": { + "tags": [ + "Conversion" + ], + "summary": "Convert Markdown to PDF", + "description": "Converts Markdown content to HTML and then to a PDF document.", + "security": [ + { + "BearerAuth": [] + }, + { + "ApiKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "required": [ + "markdown" + ], + "properties": { + "markdown": { + "type": "string", + "description": "Markdown content to convert", + "example": "# Hello World\\n\\nThis is **bold** and *italic*." + }, + "css": { + "type": "string", + "description": "Optional CSS to inject into the rendered HTML" + } + } + }, + { + "$ref": "#/components/schemas/PdfOptions" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "PDF document", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Missing markdown field" + }, + "401": { + "description": "Missing API key" + }, + "403": { + "description": "Invalid API key" + }, + "415": { + "description": "Unsupported Content-Type" + }, + "429": { + "description": "Rate limit or usage limit exceeded" + }, + "500": { + "description": "PDF generation failed" + } + } + } + }, + "/v1/convert/url": { + "post": { + "tags": [ + "Conversion" + ], + "summary": "Convert URL to PDF", + "description": "Fetches a URL and converts the rendered page to PDF. JavaScript is disabled for security.\nPrivate/internal IP addresses are blocked (SSRF protection). DNS is pinned to prevent rebinding.\n", + "security": [ + { + "BearerAuth": [] + }, + { + "ApiKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "URL to convert (http or https only)", + "example": "https://example.com" + }, + "waitUntil": { + "type": "string", + "enum": [ + "load", + "domcontentloaded", + "networkidle0", + "networkidle2" + ], + "default": "domcontentloaded", + "description": "When to consider navigation finished" + } + } + }, + { + "$ref": "#/components/schemas/PdfOptions" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "PDF document", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Missing/invalid URL or URL resolves to private IP" + }, + "401": { + "description": "Missing API key" + }, + "403": { + "description": "Invalid API key" + }, + "415": { + "description": "Unsupported Content-Type" + }, + "429": { + "description": "Rate limit or usage limit exceeded" + }, + "500": { + "description": "PDF generation failed" + } + } + } + }, + "/v1/demo/html": { + "post": { + "tags": [ + "Demo" + ], + "summary": "Convert HTML to PDF (demo)", + "description": "Public endpoint — no API key required. Rate limited to 5 requests per hour per IP.\nOutput PDFs include a DocFast watermark. Upgrade to Pro for clean output.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "required": [ + "html" + ], + "properties": { + "html": { + "type": "string", + "description": "HTML content to convert", + "example": "

Hello World

My first PDF

" + }, + "css": { + "type": "string", + "description": "Optional CSS to inject (used when html is a fragment)" + } + } + }, + { + "$ref": "#/components/schemas/PdfOptions" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Watermarked PDF document", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Missing html field", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "415": { + "description": "Unsupported Content-Type" + }, + "429": { + "description": "Demo rate limit exceeded (5/hour)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "503": { + "description": "Server busy" + }, + "504": { + "description": "PDF generation timed out" + } + } + } + }, + "/v1/demo/markdown": { + "post": { + "tags": [ + "Demo" + ], + "summary": "Convert Markdown to PDF (demo)", + "description": "Public endpoint — no API key required. Rate limited to 5 requests per hour per IP.\nMarkdown is converted to HTML then rendered to PDF with a DocFast watermark.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "required": [ + "markdown" + ], + "properties": { + "markdown": { + "type": "string", + "description": "Markdown content to convert", + "example": "# Hello World\\n\\nThis is **bold** and *italic*." + }, + "css": { + "type": "string", + "description": "Optional CSS to inject" + } + } + }, + { + "$ref": "#/components/schemas/PdfOptions" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Watermarked PDF document", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Missing markdown field" + }, + "415": { + "description": "Unsupported Content-Type" + }, + "429": { + "description": "Demo rate limit exceeded (5/hour)" + }, + "503": { + "description": "Server busy" + }, + "504": { + "description": "PDF generation timed out" + } + } + } + }, + "/health": { + "get": { + "tags": [ + "System" + ], + "summary": "Health check", + "description": "Returns service health status including database connectivity and browser pool stats.", + "responses": { + "200": { + "description": "Service is healthy", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "degraded" + ] + }, + "version": { + "type": "string", + "example": "0.4.0" + }, + "database": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "error" + ] + }, + "version": { + "type": "string", + "example": "PostgreSQL 17.4" + } + } + }, + "pool": { + "type": "object", + "properties": { + "size": { + "type": "integer" + }, + "active": { + "type": "integer" + }, + "available": { + "type": "integer" + }, + "queueDepth": { + "type": "integer" + }, + "pdfCount": { + "type": "integer" + }, + "restarting": { + "type": "boolean" + }, + "uptimeSeconds": { + "type": "integer" + } + } + } + } + } + } + } + }, + "503": { + "description": "Service is degraded (database issue)" + } + } + } + }, + "/v1/recover": { + "post": { + "tags": [ + "Account" + ], + "summary": "Request API key recovery", + "description": "Sends a 6-digit verification code to the email address if an account exists.\nResponse is always the same regardless of whether the email exists (to prevent enumeration).\nRate limited to 3 requests per hour.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email address associated with the API key" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Recovery code sent (or no-op if email not found)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "recovery_sent" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "Invalid email format" + }, + "429": { + "description": "Too many recovery attempts" + } + } + } + }, + "/v1/recover/verify": { + "post": { + "tags": [ + "Account" + ], + "summary": "Verify recovery code and retrieve API key", + "description": "Verifies the 6-digit code sent via email and returns the API key if valid. Code expires after 15 minutes.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "email", + "code" + ], + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "code": { + "type": "string", + "pattern": "^\\d{6}$", + "description": "6-digit verification code" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "API key recovered", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "recovered" + }, + "apiKey": { + "type": "string", + "description": "The recovered API key" + }, + "tier": { + "type": "string", + "enum": [ + "free", + "pro" + ] + } + } + } + } + } + }, + "400": { + "description": "Invalid verification code or missing fields" + }, + "410": { + "description": "Verification code expired" + }, + "429": { + "description": "Too many failed attempts" + } + } + } + }, + "/v1/templates": { + "get": { + "tags": [ + "Templates" + ], + "summary": "List available templates", + "description": "Returns a list of all built-in document templates with their required fields.", + "security": [ + { + "BearerAuth": [] + }, + { + "ApiKeyHeader": [] + } + ], + "responses": { + "200": { + "description": "List of templates", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "templates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "invoice" + }, + "name": { + "type": "string", + "example": "Invoice" + }, + "description": { + "type": "string" + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "description": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Missing API key" + }, + "403": { + "description": "Invalid API key" + } + } + } + }, + "/v1/templates/{id}/render": { + "post": { + "tags": [ + "Templates" + ], + "summary": "Render a template to PDF", + "description": "Renders a built-in template with the provided data and returns a PDF.\nUse GET /v1/templates to see available templates and their required fields.\nSpecial fields: `_format` (page size), `_margin` (page margins), `_filename` (output filename).\n", + "security": [ + { + "BearerAuth": [] + }, + { + "ApiKeyHeader": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + }, + "description": "Template ID (e.g. \"invoice\", \"receipt\")" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "description": "Template data (fields depend on template). Can also be passed at root level." + }, + "_format": { + "type": "string", + "enum": [ + "A4", + "Letter", + "Legal", + "A3", + "A5", + "Tabloid" + ], + "default": "A4", + "description": "Page size override" + }, + "_margin": { + "type": "object", + "properties": { + "top": { + "type": "string" + }, + "right": { + "type": "string" + }, + "bottom": { + "type": "string" + }, + "left": { + "type": "string" + } + }, + "description": "Page margin override" + }, + "_filename": { + "type": "string", + "description": "Custom output filename" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "PDF document", + "content": { + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Missing required template fields" + }, + "401": { + "description": "Missing API key" + }, + "403": { + "description": "Invalid API key" + }, + "404": { + "description": "Template not found" + }, + "500": { + "description": "Template rendering failed" + } + } + } + }, + "/v1/signup/free": { + "post": { + "tags": [ + "Account" + ], + "summary": "Free signup (discontinued)", + "description": "Free accounts have been discontinued. Use the demo endpoint for testing\nor subscribe to Pro for production use.\n", + "deprecated": true, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + } + } + } + } + }, + "responses": { + "410": { + "description": "Free accounts discontinued", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Free accounts have been discontinued." + }, + "demo_endpoint": { + "type": "string", + "example": "/v1/demo/html" + }, + "pro_url": { + "type": "string", + "example": "https://docfast.dev/#pricing" + } + } + } + } + } + } + } + } + }, + "/v1/usage": { + "get": { + "tags": [ + "System" + ], + "summary": "Usage statistics (admin only)", + "description": "Returns usage statistics for the authenticated user. Requires admin API key.", + "security": [ + { + "BearerAuth": [] + }, + { + "ApiKeyHeader": [] + } + ], + "responses": { + "200": { + "description": "Usage statistics", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "month": { + "type": "string" + } + } + } + } + } + } + }, + "403": { + "description": "Admin access required" + }, + "503": { + "description": "Admin access not configured" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1edc2e2..5b0b284 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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: diff --git a/src/swagger.ts b/src/swagger.ts index 7a605db..fdf31d6 100644 --- a/src/swagger.ts +++ b/src/swagger.ts @@ -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 ` or `X-API-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 ` or `X-API-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",