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(` +
A Pro API key has already been created for this purchase.
+If you lost your key, use the key recovery feature.
+ +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": "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