diff --git a/public/index.html.backup-20260214-175429 b/public/index.html.backup-20260214-175429 new file mode 100644 index 0000000..6523c61 --- /dev/null +++ b/public/index.html.backup-20260214-175429 @@ -0,0 +1,325 @@ + + + + + +DocFast — HTML & Markdown to PDF API + + + + + + + + + + + +
+
+
🚀 Simple PDF API for Developers
+

HTML to PDF
in one API call

+

Convert HTML, Markdown, or URLs to pixel-perfect PDFs. Built-in templates for invoices & receipts. No headless browser headaches.

+
+ + Read the Docs +
+ +
+
+
+ terminal +
+
+# Convert HTML to PDF — it's that simple +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>Your first PDF</p>"}' \ + -o output.pdf +
+
+
+
+ +
+
+
+
+
<1s
+
Avg. generation time
+
+
+
99.9%
+
Uptime SLA
+
+
+
HTTPS
+
Encrypted & secure
+
+
+
0 bytes
+
Data stored on disk
+
+
+
+
+ +
+
+

Everything you need

+

A complete PDF generation API. No SDKs, no dependencies, no setup.

+
+
+
+

Sub-second Speed

+

Persistent browser pool — no cold starts. Your PDFs are ready before your spinner shows.

+
+
+
🎨
+

Pixel-perfect Output

+

Full CSS support including flexbox, grid, and custom fonts. Your brand, your PDFs.

+
+
+
📄
+

Built-in Templates

+

Invoice and receipt templates out of the box. Pass JSON data, get beautiful PDFs.

+
+
+
🔧
+

Dead-simple API

+

REST API. JSON in, PDF out. Works with curl, Python, Node, Go — anything with HTTP.

+
+
+
📐
+

Fully Configurable

+

A4, Letter, custom sizes. Portrait or landscape. Headers, footers, and margins.

+
+
+
🔒
+

Secure by Default

+

HTTPS only. Rate limiting. No data stored. PDFs stream directly — nothing touches disk.

+
+
+
+
+ +
+
+

Simple, transparent pricing

+

Start free. Upgrade when you're ready. No surprise charges.

+
+
+
Free
+
$0 /mo
+
Perfect for side projects and testing
+
    +
  • 100 PDFs per month
  • +
  • All conversion endpoints
  • +
  • All templates included
  • +
  • Rate limiting: 10 req/min
  • +
+ +
+ +
+
+
+ + + + + + + + + diff --git a/src/index.ts b/src/index.ts index d1d45bf..8faeb16 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { convertRouter } from "./routes/convert.js"; import { templatesRouter } from "./routes/templates.js"; import { healthRouter } from "./routes/health.js"; import { signupRouter } from "./routes/signup.js"; +import { recoverRouter } from "./routes/recover.js"; import { billingRouter } from "./routes/billing.js"; import { authMiddleware } from "./middleware/auth.js"; import { usageMiddleware } from "./middleware/usage.js"; @@ -67,6 +68,7 @@ app.use(limiter); // Public routes app.use("/health", healthRouter); app.use("/v1/signup", signupRouter); +app.use("/v1/recover", recoverRouter); app.use("/v1/billing", billingRouter); // Authenticated routes diff --git a/src/routes/recover.ts b/src/routes/recover.ts new file mode 100644 index 0000000..4b043bd --- /dev/null +++ b/src/routes/recover.ts @@ -0,0 +1,90 @@ +import { Router, Request, Response } from "express"; +import rateLimit from "express-rate-limit"; +import { createPendingVerification, verifyCode } from "../services/verification.js"; +import { sendRecoveryEmail, sendVerificationEmail } from "../services/email.js"; +import { getAllKeys } from "../services/keys.js"; + +const router = Router(); + +const recoverLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, + max: 3, + message: { error: "Too many recovery attempts. Please try again in 1 hour." }, + standardHeaders: true, + legacyHeaders: false, +}); + +// Step 1: Request recovery — sends verification code +router.post("/", recoverLimiter, async (req: Request, res: Response) => { + const { email } = req.body || {}; + + if (!email || typeof email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + res.status(400).json({ error: "A valid email address is required." }); + return; + } + + const cleanEmail = email.trim().toLowerCase(); + + // Check if this email has any keys + const keys = getAllKeys(); + const userKey = keys.find(k => k.email === cleanEmail); + + // Always return success to prevent email enumeration + if (!userKey) { + res.json({ status: "recovery_sent", message: "If an account exists for this email, a verification code has been sent." }); + return; + } + + const pending = createPendingVerification(cleanEmail); + + sendVerificationEmail(cleanEmail, pending.code).catch(err => { + console.error(`Failed to send recovery email to ${cleanEmail}:`, err); + }); + + res.json({ status: "recovery_sent", message: "If an account exists for this email, a verification code has been sent." }); +}); + +// Step 2: Verify code — sends API key via email (NOT in response) +router.post("/verify", recoverLimiter, async (req: Request, res: Response) => { + const { email, code } = req.body || {}; + + if (!email || !code) { + res.status(400).json({ error: "Email and code are required." }); + return; + } + + const cleanEmail = email.trim().toLowerCase(); + const cleanCode = String(code).trim(); + + const result = verifyCode(cleanEmail, cleanCode); + + switch (result.status) { + case "ok": { + const keys = getAllKeys(); + const userKey = keys.find(k => k.email === cleanEmail); + + if (userKey) { + sendRecoveryEmail(cleanEmail, userKey.key).catch(err => { + console.error(`Failed to send recovery key to ${cleanEmail}:`, err); + }); + } + + res.json({ + status: "recovered", + message: "Your API key has been sent to your email address.", + }); + break; + } + case "expired": + res.status(410).json({ error: "Verification code has expired. Please request a new one." }); + break; + case "max_attempts": + res.status(429).json({ error: "Too many failed attempts. Please request a new code." }); + break; + case "invalid": + res.status(400).json({ error: "Invalid verification code." }); + break; + } +}); + +export { router as recoverRouter }; diff --git a/src/services/email.ts b/src/services/email.ts index 04372c2..5c62043 100644 --- a/src/services/email.ts +++ b/src/services/email.ts @@ -25,3 +25,19 @@ export async function sendVerificationEmail(email: string, code: string): Promis return false; } } + +export async function sendRecoveryEmail(email: string, apiKey: string): Promise { + try { + const info = await transporter.sendMail({ + from: "DocFast ", + to: email, + subject: "DocFast - Your API Key Recovery", + text: `Here is your DocFast API key:\n\n${apiKey}\n\nKeep this key safe. Do not share it with anyone.\n\nIf you didn't request this recovery, please ignore this email — your key has not been changed.`, + }); + console.log(`📧 Recovery email sent to ${email}: ${info.messageId}`); + return true; + } catch (err) { + console.error(`📧 Failed to send recovery email to ${email}:`, err); + return false; + } +}