diff --git "a/\001@" "b/\001@" new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index 1792349..6b3c4c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,8 +18,8 @@ services: - SMTP_HOST=host.docker.internal - SMTP_PORT=25 - POOL_SIZE=15 - - BROWSER_COUNT=2 - - PAGES_PER_BROWSER=8 + - BROWSER_COUNT=1 + - PAGES_PER_BROWSER=15 volumes: - docfast-data:/app/data mem_limit: 2560m diff --git a/package-lock.json b/package-lock.json index 1993739..1a0073c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "nanoid": "^5.0.0", "nodemailer": "^8.0.1", "puppeteer": "^24.0.0", - "stripe": "^20.3.1" + "stripe": "^20.3.1", + "swagger-ui-dist": "^5.31.0" }, "devDependencies": { "@types/express": "^5.0.0", @@ -892,6 +893,12 @@ "win32" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -3414,6 +3421,14 @@ } } }, + "node_modules/swagger-ui-dist": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/tar-fs": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", diff --git a/package.json b/package.json index 6bb2ff8..3cc82fc 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "nanoid": "^5.0.0", "nodemailer": "^8.0.1", "puppeteer": "^24.0.0", - "stripe": "^20.3.1" + "stripe": "^20.3.1", + "swagger-ui-dist": "^5.31.0" }, "devDependencies": { "@types/express": "^5.0.0", diff --git a/public/app.js b/public/app.js index 8643052..9841160 100644 --- a/public/app.js +++ b/public/app.js @@ -298,3 +298,143 @@ document.addEventListener('DOMContentLoaded', function() { }); }); }); + +// --- Email Change --- +var emailChangeApiKey = ''; +var emailChangeNewEmail = ''; + +function showEmailChangeState(state) { + ['emailChangeInitial', 'emailChangeLoading', 'emailChangeVerify', 'emailChangeResult'].forEach(function(id) { + var el = document.getElementById(id); + if (el) el.classList.remove('active'); + }); + document.getElementById(state).classList.add('active'); +} + +function openEmailChange() { + closeSignup(); + closeRecover(); + document.getElementById('emailChangeModal').classList.add('active'); + showEmailChangeState('emailChangeInitial'); + var errEl = document.getElementById('emailChangeError'); + if (errEl) errEl.style.display = 'none'; + var verifyErrEl = document.getElementById('emailChangeVerifyError'); + if (verifyErrEl) verifyErrEl.style.display = 'none'; + document.getElementById('emailChangeApiKey').value = ''; + document.getElementById('emailChangeNewEmail').value = ''; + document.getElementById('emailChangeCode').value = ''; + emailChangeApiKey = ''; + emailChangeNewEmail = ''; +} + +function closeEmailChange() { + document.getElementById('emailChangeModal').classList.remove('active'); +} + +async function submitEmailChange() { + var errEl = document.getElementById('emailChangeError'); + var btn = document.getElementById('emailChangeBtn'); + var apiKey = document.getElementById('emailChangeApiKey').value.trim(); + var newEmail = document.getElementById('emailChangeNewEmail').value.trim(); + + if (!apiKey) { + errEl.textContent = 'Please enter your API key.'; + errEl.style.display = 'block'; + return; + } + if (!newEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) { + errEl.textContent = 'Please enter a valid email address.'; + errEl.style.display = 'block'; + return; + } + + errEl.style.display = 'none'; + btn.disabled = true; + showEmailChangeState('emailChangeLoading'); + + try { + var res = await fetch('/v1/email-change', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ apiKey: apiKey, newEmail: newEmail }) + }); + var data = await res.json(); + + if (!res.ok) { + showEmailChangeState('emailChangeInitial'); + errEl.textContent = data.error || 'Something went wrong.'; + errEl.style.display = 'block'; + btn.disabled = false; + return; + } + + emailChangeApiKey = apiKey; + emailChangeNewEmail = newEmail; + document.getElementById('emailChangeEmailDisplay').textContent = newEmail; + showEmailChangeState('emailChangeVerify'); + document.getElementById('emailChangeCode').focus(); + btn.disabled = false; + } catch (err) { + showEmailChangeState('emailChangeInitial'); + errEl.textContent = 'Network error. Please try again.'; + errEl.style.display = 'block'; + btn.disabled = false; + } +} + +async function submitEmailChangeVerify() { + var errEl = document.getElementById('emailChangeVerifyError'); + var btn = document.getElementById('emailChangeVerifyBtn'); + var code = document.getElementById('emailChangeCode').value.trim(); + + if (!code || !/^\d{6}$/.test(code)) { + errEl.textContent = 'Please enter a 6-digit code.'; + errEl.style.display = 'block'; + return; + } + + errEl.style.display = 'none'; + btn.disabled = true; + + try { + var res = await fetch('/v1/email-change/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ apiKey: emailChangeApiKey, newEmail: emailChangeNewEmail, code: code }) + }); + var data = await res.json(); + + if (!res.ok) { + errEl.textContent = data.error || 'Verification failed.'; + errEl.style.display = 'block'; + btn.disabled = false; + return; + } + + document.getElementById('emailChangeNewDisplay').textContent = data.newEmail || emailChangeNewEmail; + showEmailChangeState('emailChangeResult'); + } catch (err) { + errEl.textContent = 'Network error. Please try again.'; + errEl.style.display = 'block'; + btn.disabled = false; + } +} + +// Add event listeners for email change (append to DOMContentLoaded) +document.addEventListener('DOMContentLoaded', function() { + var closeBtn = document.getElementById('btn-close-email-change'); + if (closeBtn) closeBtn.addEventListener('click', closeEmailChange); + + var changeBtn = document.getElementById('emailChangeBtn'); + if (changeBtn) changeBtn.addEventListener('click', submitEmailChange); + + var verifyBtn = document.getElementById('emailChangeVerifyBtn'); + if (verifyBtn) verifyBtn.addEventListener('click', submitEmailChangeVerify); + + var modal = document.getElementById('emailChangeModal'); + if (modal) modal.addEventListener('click', function(e) { if (e.target === this) closeEmailChange(); }); + + document.querySelectorAll('.open-email-change').forEach(function(el) { + el.addEventListener('click', function(e) { e.preventDefault(); openEmailChange(); }); + }); +}); diff --git a/public/docs.html b/public/docs.html index 8ec3556..26d0a6a 100644 --- a/public/docs.html +++ b/public/docs.html @@ -4,393 +4,125 @@ DocFast API Documentation + + + + -
-
-

DocFast API Documentation

-

Convert HTML, Markdown, and URLs to PDF. Built-in invoice & receipt templates.

-
Base URL: https://docfast.dev
-
- - - -
-

Authentication

-

All conversion and template endpoints require an API key. Pass it using either method:

-
Authorization: Bearer df_free_your_api_key_here
-

Or use the X-API-Key header:

-
X-API-Key: df_free_your_api_key_here
-

Get a free API key instantly — no credit card required:

-
curl -X POST https://docfast.dev/v1/signup/free \
-  -H "Content-Type: application/json" \
-  -d '{"email": "you@example.com"}'
-
Free tier: 100 PDFs/month. Pro ($9/mo): 10,000 PDFs/month. Upgrade anytime at docfast.dev.
-
- -
-

Convert HTML to PDF

-
-
- POST - /v1/convert/html -
-

Convert raw HTML (with optional CSS) to a PDF document.

- -

Request Body

- - - - - - - -
FieldTypeDescription
html requiredstringHTML content to convert
cssstringAdditional CSS to inject
formatstringPage size: A4 (default), Letter, Legal, A3
landscapebooleanLandscape orientation (default: false)
marginobject{top, right, bottom, left} in CSS units (default: 20mm each)
- -

Example

-
curl -X POST https://docfast.dev/v1/convert/html \
-  -H "Authorization: Bearer YOUR_KEY" \
-  -H "Content-Type: application/json" \
-  -d '{
-    "html": "<h1>Hello World</h1><p>Generated by DocFast.</p>",
-    "css": "h1 { color: navy; }",
-    "format": "A4"
-  }' \
-  -o output.pdf
- -

Response

-

200 OK — Returns the PDF as application/pdf binary stream.

-
- 200 PDF generated - 400 Missing html field - 401 Invalid/missing API key - 429 Rate limited -
-
-
- -
-

Convert Markdown to PDF

-
-
- POST - /v1/convert/markdown -
-

Convert Markdown to a styled PDF with syntax highlighting for code blocks.

- -

Request Body

- - - - - - - -
FieldTypeDescription
markdown requiredstringMarkdown content
cssstringAdditional CSS to inject
formatstringPage size (default: A4)
landscapebooleanLandscape orientation
marginobjectCustom margins
- -

Example

-
curl -X POST https://docfast.dev/v1/convert/markdown \
-  -H "Authorization: Bearer YOUR_KEY" \
-  -H "Content-Type: application/json" \
-  -d '{
-    "markdown": "# Monthly Report\n\n## Summary\n\nRevenue increased by **15%** this quarter.\n\n| Metric | Value |\n|--------|-------|\n| Users  | 1,234 |\n| MRR    | $5,670 |"
-  }' \
-  -o report.pdf
- -

Response

-

200 OK — Returns application/pdf.

-
- 200 PDF generated - 400 Missing markdown field - 401 Invalid/missing API key -
-
-
- -
-

Convert URL to PDF

-
-
- POST - /v1/convert/url -
-

Navigate to a URL and convert the rendered page to PDF. Supports JavaScript-rendered pages.

- -

Request Body

- - - - - - - -
FieldTypeDescription
url requiredstringURL to convert (must start with http:// or https://)
waitUntilstringload (default), domcontentloaded, networkidle0, networkidle2
formatstringPage size (default: A4)
landscapebooleanLandscape orientation
marginobjectCustom margins
- -

Example

-
curl -X POST https://docfast.dev/v1/convert/url \
-  -H "Authorization: Bearer YOUR_KEY" \
-  -H "Content-Type: application/json" \
-  -d '{
-    "url": "https://example.com",
-    "waitUntil": "networkidle0",
-    "format": "Letter"
-  }' \
-  -o page.pdf
- -

Response

-

200 OK — Returns application/pdf.

-
- 200 PDF generated - 400 Missing or invalid URL - 401 Invalid/missing API key -
-
-
- -
-

List Templates

-
-
- GET - /v1/templates -
-

List all available document templates with their field definitions.

- -

Example

-
curl https://docfast.dev/v1/templates \
-  -H "Authorization: Bearer YOUR_KEY"
- -

Response

-
{
-  "templates": [
-    {
-      "id": "invoice",
-      "name": "Invoice",
-      "description": "Professional invoice with line items, taxes, and payment details",
-      "fields": [
-        {"name": "invoiceNumber", "type": "string", "required": true},
-        {"name": "date", "type": "string", "required": true},
-        {"name": "from", "type": "object", "required": true, "description": "Sender: {name, address?, email?, phone?, vatId?}"},
-        {"name": "to", "type": "object", "required": true, "description": "Recipient: {name, address?, email?, vatId?}"},
-        {"name": "items", "type": "array", "required": true, "description": "Line items: [{description, quantity, unitPrice, taxRate?}]"},
-        {"name": "currency", "type": "string", "required": false},
-        {"name": "notes", "type": "string", "required": false},
-        {"name": "paymentDetails", "type": "string", "required": false}
-      ]
-    },
-    {
-      "id": "receipt",
-      "name": "Receipt",
-      "description": "Simple receipt for payments received",
-      "fields": [ ... ]
-    }
-  ]
-}
-
-
- -
-

Render Template

-
-
- POST - /v1/templates/:id/render -
-

Render a template with your data and get a PDF. No HTML needed — just pass structured data.

- -

Path Parameters

- - - -
ParamDescription
:idTemplate ID (invoice or receipt)
- -

Request Body

- - - -
FieldTypeDescription
data requiredobjectTemplate data (see field definitions from /v1/templates)
- -

Invoice Example

-
curl -X POST https://docfast.dev/v1/templates/invoice/render \
-  -H "Authorization: Bearer YOUR_KEY" \
-  -H "Content-Type: application/json" \
-  -d '{
-    "data": {
-      "invoiceNumber": "INV-2026-001",
-      "date": "2026-02-14",
-      "dueDate": "2026-03-14",
-      "from": {
-        "name": "Acme Corp",
-        "address": "123 Main St, Vienna",
-        "email": "billing@acme.com",
-        "vatId": "ATU12345678"
-      },
-      "to": {
-        "name": "Client Inc",
-        "address": "456 Oak Ave, Berlin",
-        "email": "accounts@client.com"
-      },
-      "items": [
-        {"description": "Web Development", "quantity": 40, "unitPrice": 95, "taxRate": 20},
-        {"description": "Hosting (monthly)", "quantity": 1, "unitPrice": 29}
+  ← Back to docfast.dev
+  
+ + diff --git a/public/index.html b/public/index.html index 6335058..065cf70 100644 --- a/public/index.html +++ b/public/index.html @@ -195,6 +195,13 @@ html, body { #recoverLoading.active { display: flex; flex-direction: column; align-items: center; padding: 40px 0; text-align: center; } #recoverResult.active { display: block; } #recoverVerify.active { display: block; } + +/* Email change modal states */ +#emailChangeInitial, #emailChangeLoading, #emailChangeVerify, #emailChangeResult { display: none; } +#emailChangeInitial.active { display: block; } +#emailChangeLoading.active { display: flex; flex-direction: column; align-items: center; padding: 40px 0; text-align: center; } +#emailChangeResult.active { display: block; } +#emailChangeVerify.active { display: block; } @@ -222,6 +229,7 @@ html, body { Read the Docs
+

Already have an account? Lost your API key? Recover it →

@@ -341,6 +349,7 @@ html, body {
@@ -432,6 +441,44 @@ html, body {
+ + + + diff --git a/public/openapi.json b/public/openapi.json new file mode 100644 index 0000000..811ada6 --- /dev/null +++ b/public/openapi.json @@ -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 ` or `X-API-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": "

Hello World

Your first PDF

" }, + "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" } + } + } + } + } +} diff --git a/public/swagger-ui b/public/swagger-ui new file mode 120000 index 0000000..51eba1f --- /dev/null +++ b/public/swagger-ui @@ -0,0 +1 @@ +/opt/docfast/node_modules/swagger-ui-dist \ No newline at end of file