From 8fddc6009e0a61d877994c3d98e4294bb2bed793 Mon Sep 17 00:00:00 2001 From: Hoid Date: Sat, 14 Feb 2026 14:20:32 +0000 Subject: [PATCH] session 11: fixed signup flows, unified key store --- projects/business/memory/sessions.md | 15 ++ projects/business/memory/state.json | 10 +- projects/business/src/pdf-api/.gitignore | 1 + projects/business/src/pdf-api/dist/index.js | 26 ++-- .../src/pdf-api/dist/middleware/auth.js | 6 +- .../business/src/pdf-api/docker-compose.yml | 5 + .../business/src/pdf-api/public/index.html | 133 ++++++++++++++-- projects/business/src/pdf-api/src/index.ts | 28 ++-- .../src/pdf-api/src/middleware/auth.ts | 9 +- .../src/pdf-api/src/middleware/usage.ts | 14 +- .../src/pdf-api/src/routes/billing.ts | 146 +++++++----------- .../business/src/pdf-api/src/routes/signup.ts | 33 ++++ .../business/src/pdf-api/src/services/keys.ts | 118 ++++++++++++++ 13 files changed, 401 insertions(+), 143 deletions(-) create mode 100644 projects/business/src/pdf-api/src/routes/signup.ts create mode 100644 projects/business/src/pdf-api/src/services/keys.ts diff --git a/projects/business/memory/sessions.md b/projects/business/memory/sessions.md index 36d782f..f76b156 100644 --- a/projects/business/memory/sessions.md +++ b/projects/business/memory/sessions.md @@ -117,3 +117,18 @@ - **Status:** DocFast is LIVE. Fully functional API with SSL, billing, landing page. - **Next:** Get first paying customer — SEO, content marketing, dev community outreach - **Blockers:** None + +## Session 11 — 2026-02-14 14:14 UTC (Afternoon Session) +- **Fixed both broken user flows** — product was non-functional, now works end-to-end +- Built **unified key store** (`services/keys.ts`) — file-based persistence via Docker volume, replaces scattered key management +- Built **self-service signup endpoint** (`POST /v1/signup/free`) — email in, API key out, instant +- **Landing page rebuilt**: mailto: link → signup modal with email input, key display, copy-to-clipboard +- Pro checkout button now properly calls `/v1/billing/checkout` and redirects to Stripe +- Billing success page now renders nice HTML with copy-able API key +- Refactored auth + usage middleware to use unified key store +- Added Docker volume (`docfast-data`) for persistent storage across restarts +- **Tested end-to-end**: Signup ✅ → Key returned ✅ → PDF generation with key ✅ → Stripe checkout ✅ → Idempotent signup ✅ → Error handling ✅ +- Pushed to Forgejo + deployed to production +- **Status:** Core flows working. Need full QA pass via browser before declaring Phase 2 ready. +- **Next:** Browser-based QA of entire user journey, then Phase 2 (marketing/customers) +- **Blockers:** None diff --git a/projects/business/memory/state.json b/projects/business/memory/state.json index 3dbab69..d012d1b 100644 --- a/projects/business/memory/state.json +++ b/projects/business/memory/state.json @@ -1,9 +1,9 @@ { "phase": 1, - "phaseLabel": "Build MVP — Fix & Polish", - "status": "broken-needs-fixing", + "phaseLabel": "Build MVP — QA & Polish", + "status": "testing-before-launch", "product": "DocFast — HTML/Markdown to PDF API", - "currentPriority": "PRODUCT IS BROKEN. Human tested and found: (1) 'Get Free API Key' is just a mailto: link — needs to be a real self-service signup flow that generates and returns an API key. (2) 'Get Started' for Pro plan does nothing. FIX THESE. Build actual signup/key provisioning. Test the ENTIRE user flow yourself before declaring anything done. Do NOT move to Phase 2 until every button works and a user can go from landing page → API key → working PDF without any manual intervention.", + "currentPriority": "Full QA pass: test every user flow end-to-end via browser. Signup modal UX, Pro checkout redirect, PDF downloads, error states. Fix any issues found. Only move to Phase 2 when everything works flawlessly.", "infrastructure": { "domain": "docfast.dev", "url": "https://docfast.dev", @@ -13,7 +13,7 @@ "serverIP": "167.235.156.214", "sshKey": "/home/openclaw/.ssh/docfast", "ssl": "Let's Encrypt (auto-renew)", - "apiKey": "df_live_9760e44a3e732be0f8628a44e0cdbc040107499f6e8f457a" + "dataVolume": "docfast-data (Docker volume for persistent key storage)" }, "credentials": { "file": "/home/openclaw/.openclaw/workspace/.credentials/docfast.env", @@ -22,5 +22,5 @@ }, "blockers": [], "startDate": "2026-02-14", - "sessionCount": 10 + "sessionCount": 11 } diff --git a/projects/business/src/pdf-api/.gitignore b/projects/business/src/pdf-api/.gitignore index aa0926a..caf428f 100644 --- a/projects/business/src/pdf-api/.gitignore +++ b/projects/business/src/pdf-api/.gitignore @@ -2,3 +2,4 @@ node_modules/ dist/ .env *.log +data/ diff --git a/projects/business/src/pdf-api/dist/index.js b/projects/business/src/pdf-api/dist/index.js index 48ff1f4..a418dae 100644 --- a/projects/business/src/pdf-api/dist/index.js +++ b/projects/business/src/pdf-api/dist/index.js @@ -6,19 +6,23 @@ import rateLimit from "express-rate-limit"; 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 { billingRouter } from "./routes/billing.js"; import { authMiddleware } from "./middleware/auth.js"; import { usageMiddleware } from "./middleware/usage.js"; import { getUsageStats } from "./middleware/usage.js"; import { initBrowser, closeBrowser } from "./services/browser.js"; -import { billingRouter } from "./routes/billing.js"; +import { loadKeys, getAllKeys } from "./services/keys.js"; const app = express(); const PORT = parseInt(process.env.PORT || "3100", 10); +// Load API keys from persistent store +loadKeys(); app.use(helmet()); // Raw body for Stripe webhook signature verification app.use("/v1/billing/webhook", express.raw({ type: "application/json" })); app.use(express.json({ limit: "2mb" })); app.use(express.text({ limit: "2mb", type: "text/*" })); -// Rate limiting: 100 req/min for free tier +// Rate limiting const limiter = rateLimit({ windowMs: 60_000, max: 100, @@ -26,37 +30,39 @@ const limiter = rateLimit({ legacyHeaders: false, }); app.use(limiter); -// Public +// Public routes app.use("/health", healthRouter); -// Authenticated +app.use("/v1/signup", signupRouter); +app.use("/v1/billing", billingRouter); +// Authenticated routes app.use("/v1/convert", authMiddleware, usageMiddleware, convertRouter); app.use("/v1/templates", authMiddleware, usageMiddleware, templatesRouter); -// Billing (public — Stripe handles auth) -app.use("/v1/billing", billingRouter); -// Admin: usage stats (protected by auth) +// Admin: usage stats app.get("/v1/usage", authMiddleware, (_req, res) => { res.json(getUsageStats()); }); // Landing page const __dirname = path.dirname(fileURLToPath(import.meta.url)); app.use(express.static(path.join(__dirname, "../public"))); -// API root (for programmatic discovery) +// API root app.get("/api", (_req, res) => { res.json({ name: "DocFast API", - version: "0.1.0", - docs: "/health", + version: "0.2.0", endpoints: [ + "POST /v1/signup/free — Get a free API key", "POST /v1/convert/html", "POST /v1/convert/markdown", "POST /v1/convert/url", "POST /v1/templates/:id/render", "GET /v1/templates", + "POST /v1/billing/checkout — Start Pro subscription", ], }); }); async function start() { await initBrowser(); + console.log(`Loaded ${getAllKeys().length} API keys`); app.listen(PORT, () => console.log(`DocFast API running on :${PORT}`)); const shutdown = async () => { console.log("Shutting down..."); diff --git a/projects/business/src/pdf-api/dist/middleware/auth.js b/projects/business/src/pdf-api/dist/middleware/auth.js index b82e91d..5ba21ad 100644 --- a/projects/business/src/pdf-api/dist/middleware/auth.js +++ b/projects/business/src/pdf-api/dist/middleware/auth.js @@ -1,4 +1,4 @@ -const API_KEYS = new Set((process.env.API_KEYS || "test-key-123").split(",").map((k) => k.trim())); +import { isValidKey, getKeyInfo } from "../services/keys.js"; export function authMiddleware(req, res, next) { const header = req.headers.authorization; if (!header?.startsWith("Bearer ")) { @@ -6,9 +6,11 @@ export function authMiddleware(req, res, next) { return; } const key = header.slice(7); - if (!API_KEYS.has(key)) { + if (!isValidKey(key)) { res.status(403).json({ error: "Invalid API key" }); return; } + // Attach key info to request for downstream use + req.apiKeyInfo = getKeyInfo(key); next(); } diff --git a/projects/business/src/pdf-api/docker-compose.yml b/projects/business/src/pdf-api/docker-compose.yml index a1ec12f..9a0458c 100644 --- a/projects/business/src/pdf-api/docker-compose.yml +++ b/projects/business/src/pdf-api/docker-compose.yml @@ -13,5 +13,10 @@ services: - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - BASE_URL=${BASE_URL:-https://docfast.dev} - PRO_KEYS=${PRO_KEYS} + volumes: + - docfast-data:/app/data mem_limit: 512m cpus: 1.0 + +volumes: + docfast-data: diff --git a/projects/business/src/pdf-api/public/index.html b/projects/business/src/pdf-api/public/index.html index 4d58cd4..ec99b8d 100644 --- a/projects/business/src/pdf-api/public/index.html +++ b/projects/business/src/pdf-api/public/index.html @@ -11,7 +11,6 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; } a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } - .container { max-width: 960px; margin: 0 auto; padding: 0 24px; } /* Hero */ @@ -20,10 +19,10 @@ a:hover { text-decoration: underline; } .hero h1 span { color: var(--accent); } .hero p { font-size: 1.25rem; color: var(--muted); max-width: 600px; margin: 0 auto 40px; } .hero-actions { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; } -.btn { display: inline-block; padding: 14px 32px; border-radius: 8px; font-size: 1rem; font-weight: 600; transition: all 0.2s; } +.btn { display: inline-block; padding: 14px 32px; border-radius: 8px; font-size: 1rem; font-weight: 600; transition: all 0.2s; border: none; cursor: pointer; } .btn-primary { background: var(--accent); color: #000; } .btn-primary:hover { background: #6fb; text-decoration: none; } -.btn-secondary { border: 1px solid var(--border); color: var(--fg); } +.btn-secondary { border: 1px solid var(--border); color: var(--fg); background: transparent; } .btn-secondary:hover { border-color: var(--muted); text-decoration: none; } /* Code block */ @@ -65,6 +64,24 @@ a:hover { text-decoration: underline; } /* Footer */ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0.85rem; border-top: 1px solid var(--border); } + +/* Modal */ +.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 100; align-items: center; justify-content: center; } +.modal-overlay.active { display: flex; } +.modal { background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 40px; max-width: 440px; width: 90%; } +.modal h2 { margin-bottom: 8px; font-size: 1.5rem; } +.modal p { color: var(--muted); margin-bottom: 24px; font-size: 0.95rem; } +.modal input { width: 100%; padding: 14px 16px; border-radius: 8px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); font-size: 1rem; margin-bottom: 16px; outline: none; } +.modal input:focus { border-color: var(--accent); } +.modal .btn { width: 100%; text-align: center; } +.modal .error { color: #f66; font-size: 0.85rem; margin-bottom: 12px; display: none; } +.modal .close { position: absolute; top: 16px; right: 20px; color: var(--muted); font-size: 1.5rem; cursor: pointer; background: none; border: none; } + +/* Key result */ +.key-result { display: none; } +.key-result .key-box { background: var(--bg); border: 1px solid var(--accent); border-radius: 8px; padding: 16px; font-family: monospace; font-size: 0.85rem; word-break: break-all; margin: 16px 0; cursor: pointer; transition: background 0.2s; } +.key-result .key-box:hover { background: #111; } +.key-result .copy-hint { color: var(--muted); font-size: 0.8rem; text-align: center; } @@ -74,12 +91,12 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0.

HTML & Markdown to PDF

One API call. Beautiful PDFs. Built-in invoice templates. No headless browser setup, no dependencies, no hassle.

- Get API Key + View Docs
// Convert markdown to PDF in one call
- curl -X POST https://api.docfast.dev/v1/convert/markdown \
+ curl -X POST https://docfast.dev/v1/convert/markdown \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"markdown": "# Invoice\\n\\nAmount: $500"}' \
@@ -139,6 +156,11 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0. /v1/convert/markdown
Convert Markdown to styled PDF with syntax highlighting.
+
+ POST + /v1/convert/url +
Navigate to a URL and convert the page to PDF.
+
GET /v1/templates @@ -171,7 +193,7 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0.
  • All templates
  • Community support
  • - Get Free Key +
    @@ -195,9 +217,102 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0. + + +