feat: email change UI, Swagger UI improvements, key recovery link on landing page
- Email change modal: API key + new email → verification code → confirmed - Swagger UI with proper OpenAPI spec (public/openapi.json + swagger-ui assets) - Key recovery link prominently on landing page hero section - Footer link for email change - Updated docs.html to use swagger-ui bundle
This commit is contained in:
parent
efa39661cf
commit
d859e9fa60
9 changed files with 741 additions and 383 deletions
422
public/openapi.json
Normal file
422
public/openapi.json
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
{
|
||||
"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 <key>` or `X-API-Key: <key>` header.\n\n## Rate Limits\n- Free tier: 100 PDFs/month, 10 req/min\n- Pro tier: 10,000 PDFs/month\n\n## Getting Started\n1. Sign up at [docfast.dev](https://docfast.dev) or via `POST /v1/signup/free`\n2. Verify your email with the 6-digit code\n3. Use your API key to convert documents",
|
||||
"contact": { "name": "DocFast", "url": "https://docfast.dev" }
|
||||
},
|
||||
"servers": [{ "url": "https://docfast.dev", "description": "Production" }],
|
||||
"tags": [
|
||||
{ "name": "Conversion", "description": "Convert HTML, Markdown, or URLs to PDF" },
|
||||
{ "name": "Templates", "description": "Built-in document templates" },
|
||||
{ "name": "Account", "description": "Signup, 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", "example": "20mm" },
|
||||
"bottom": { "type": "string", "example": "20mm" },
|
||||
"left": { "type": "string", "example": "15mm" },
|
||||
"right": { "type": "string", "example": "15mm" }
|
||||
}
|
||||
},
|
||||
"printBackground": { "type": "boolean", "default": true, "description": "Print background graphics" },
|
||||
"filename": { "type": "string", "default": "document.pdf", "description": "Suggested filename for the PDF" }
|
||||
}
|
||||
},
|
||||
"Error": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": { "type": "string", "description": "Error message" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/v1/convert/html": {
|
||||
"post": {
|
||||
"tags": ["Conversion"],
|
||||
"summary": "Convert HTML to PDF",
|
||||
"description": "Renders HTML content as a PDF. Supports full CSS including flexbox, grid, and custom fonts. Bare HTML fragments are auto-wrapped.",
|
||||
"security": [{ "BearerAuth": [] }, { "ApiKeyHeader": [] }],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["html"],
|
||||
"properties": {
|
||||
"html": { "type": "string", "description": "HTML content to convert", "example": "<h1>Hello World</h1><p>Your first PDF</p>" },
|
||||
"css": { "type": "string", "description": "Optional CSS to inject (for HTML fragments)" }
|
||||
}
|
||||
},
|
||||
{ "$ref": "#/components/schemas/PdfOptions" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "PDF file", "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } } },
|
||||
"400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
|
||||
"401": { "description": "Missing or invalid API key" },
|
||||
"415": { "description": "Unsupported Content-Type" },
|
||||
"429": { "description": "Rate limit exceeded or server busy" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/convert/markdown": {
|
||||
"post": {
|
||||
"tags": ["Conversion"],
|
||||
"summary": "Convert Markdown to PDF",
|
||||
"description": "Converts Markdown content to a beautifully styled PDF with syntax highlighting.",
|
||||
"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 this is *italic*.\n\n- Item 1\n- Item 2" },
|
||||
"css": { "type": "string", "description": "Optional custom CSS" }
|
||||
}
|
||||
},
|
||||
{ "$ref": "#/components/schemas/PdfOptions" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "PDF file", "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } } },
|
||||
"400": { "description": "Invalid request" },
|
||||
"401": { "description": "Missing or invalid API key" },
|
||||
"429": { "description": "Rate limit exceeded" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/convert/url": {
|
||||
"post": {
|
||||
"tags": ["Conversion"],
|
||||
"summary": "Convert URL to PDF",
|
||||
"description": "Fetches a URL and converts the rendered page to PDF. Only http/https URLs are supported. Private/reserved IPs are blocked (SSRF protection).",
|
||||
"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", "example": "https://example.com" },
|
||||
"waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle0", "networkidle2"], "default": "networkidle0", "description": "When to consider navigation complete" }
|
||||
}
|
||||
},
|
||||
{ "$ref": "#/components/schemas/PdfOptions" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "PDF file", "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } } },
|
||||
"400": { "description": "Invalid URL or DNS failure" },
|
||||
"401": { "description": "Missing or invalid API key" },
|
||||
"429": { "description": "Rate limit exceeded" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/templates": {
|
||||
"get": {
|
||||
"tags": ["Templates"],
|
||||
"summary": "List available templates",
|
||||
"description": "Returns all available document templates with their field definitions.",
|
||||
"security": [{ "BearerAuth": [] }, { "ApiKeyHeader": [] }],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Template list",
|
||||
"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": "string" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/templates/{id}/render": {
|
||||
"post": {
|
||||
"tags": ["Templates"],
|
||||
"summary": "Render a template to PDF",
|
||||
"description": "Renders a template with the provided data and returns a PDF.",
|
||||
"security": [{ "BearerAuth": [] }, { "ApiKeyHeader": [] }],
|
||||
"parameters": [
|
||||
{ "name": "id", "in": "path", "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", "example": { "company": "Acme Corp", "items": [{ "description": "Widget", "quantity": 5, "price": 9.99 }] } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "PDF file", "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } } },
|
||||
"404": { "description": "Template not found" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/signup/free": {
|
||||
"post": {
|
||||
"tags": ["Account"],
|
||||
"summary": "Request a free API key",
|
||||
"description": "Sends a 6-digit verification code to your email. Use `/v1/signup/verify` to complete signup.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email"],
|
||||
"properties": {
|
||||
"email": { "type": "string", "format": "email", "example": "you@example.com" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Verification code sent",
|
||||
"content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "verification_required" }, "message": { "type": "string" } } } } }
|
||||
},
|
||||
"409": { "description": "Email already registered" },
|
||||
"429": { "description": "Too many signup attempts" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/signup/verify": {
|
||||
"post": {
|
||||
"tags": ["Account"],
|
||||
"summary": "Verify email and get API key",
|
||||
"description": "Verify your email with the 6-digit code to receive your API key.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email", "code"],
|
||||
"properties": {
|
||||
"email": { "type": "string", "format": "email" },
|
||||
"code": { "type": "string", "example": "123456", "description": "6-digit verification code" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "API key issued",
|
||||
"content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "verified" }, "apiKey": { "type": "string", "example": "df_free_abc123..." }, "tier": { "type": "string", "example": "free" } } } } }
|
||||
},
|
||||
"400": { "description": "Invalid code" },
|
||||
"410": { "description": "Code expired" },
|
||||
"429": { "description": "Too many attempts" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/recover": {
|
||||
"post": {
|
||||
"tags": ["Account"],
|
||||
"summary": "Request API key recovery",
|
||||
"description": "Sends a verification code to your registered email. Returns the same response whether or not the email exists (prevents enumeration).",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email"],
|
||||
"properties": {
|
||||
"email": { "type": "string", "format": "email" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "Recovery code sent (if account exists)" },
|
||||
"429": { "description": "Too many recovery attempts" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/recover/verify": {
|
||||
"post": {
|
||||
"tags": ["Account"],
|
||||
"summary": "Verify recovery code and get API key",
|
||||
"description": "Verify the recovery code to retrieve your API key. The key is shown only in the response — never sent via email.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email", "code"],
|
||||
"properties": {
|
||||
"email": { "type": "string", "format": "email" },
|
||||
"code": { "type": "string", "example": "123456" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "API key recovered",
|
||||
"content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "recovered" }, "apiKey": { "type": "string" }, "tier": { "type": "string" } } } } }
|
||||
},
|
||||
"400": { "description": "Invalid code" },
|
||||
"410": { "description": "Code expired" },
|
||||
"429": { "description": "Too many attempts" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/email-change": {
|
||||
"post": {
|
||||
"tags": ["Account"],
|
||||
"summary": "Request email change",
|
||||
"description": "Change the email associated with your API key. Sends a verification code to the new email address.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["apiKey", "newEmail"],
|
||||
"properties": {
|
||||
"apiKey": { "type": "string", "description": "Your current API key" },
|
||||
"newEmail": { "type": "string", "format": "email", "description": "New email address" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "Verification code sent to new email" },
|
||||
"400": { "description": "Invalid input" },
|
||||
"401": { "description": "Invalid API key" },
|
||||
"409": { "description": "Email already in use" },
|
||||
"429": { "description": "Too many attempts" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/email-change/verify": {
|
||||
"post": {
|
||||
"tags": ["Account"],
|
||||
"summary": "Verify email change",
|
||||
"description": "Verify the code sent to your new email to complete the change.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["apiKey", "newEmail", "code"],
|
||||
"properties": {
|
||||
"apiKey": { "type": "string" },
|
||||
"newEmail": { "type": "string", "format": "email" },
|
||||
"code": { "type": "string", "example": "123456" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "Email updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "updated" }, "newEmail": { "type": "string" } } } } } },
|
||||
"400": { "description": "Invalid code" },
|
||||
"401": { "description": "Invalid API key" },
|
||||
"410": { "description": "Code expired" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/billing/checkout": {
|
||||
"post": {
|
||||
"tags": ["Billing"],
|
||||
"summary": "Start Pro subscription checkout",
|
||||
"description": "Creates a Stripe Checkout session for the Pro plan ($9/mo). Returns a URL to redirect the user to.",
|
||||
"responses": {
|
||||
"200": { "description": "Checkout URL", "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" } } } } } },
|
||||
"500": { "description": "Checkout creation failed" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/usage": {
|
||||
"get": {
|
||||
"tags": ["System"],
|
||||
"summary": "Get usage statistics",
|
||||
"description": "Returns your API usage statistics for the current billing period.",
|
||||
"security": [{ "BearerAuth": [] }, { "ApiKeyHeader": [] }],
|
||||
"responses": {
|
||||
"200": { "description": "Usage stats" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/health": {
|
||||
"get": {
|
||||
"tags": ["System"],
|
||||
"summary": "Health check",
|
||||
"description": "Returns service health status. No authentication required.",
|
||||
"responses": {
|
||||
"200": { "description": "Service is healthy" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue