fix: self-service signup, unified key store, persistent data volume
- Added /v1/signup/free endpoint for instant API key provisioning - Built unified key store (services/keys.ts) with file-based persistence - Refactored auth middleware to use key store (no more hardcoded env keys) - Refactored usage middleware to check key tier from store - Updated billing to use key store for Pro key provisioning - Landing page: replaced mailto: link with signup modal - Landing page: Pro checkout button now properly calls /v1/billing/checkout - Added Docker volume for persistent key storage - Success page now renders HTML instead of raw JSON - Tested: signup → key → PDF generation works end-to-end
This commit is contained in:
parent
c12c1176b0
commit
467a97ae1c
9 changed files with 361 additions and 126 deletions
|
|
@ -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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -74,12 +91,12 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0.
|
|||
<h1>HTML & Markdown to <span>PDF</span></h1>
|
||||
<p>One API call. Beautiful PDFs. Built-in invoice templates. No headless browser setup, no dependencies, no hassle.</p>
|
||||
<div class="hero-actions">
|
||||
<a href="#pricing" class="btn btn-primary">Get API Key</a>
|
||||
<button class="btn btn-primary" onclick="openSignup()">Get Free API Key</button>
|
||||
<a href="#endpoints" class="btn btn-secondary">View Docs</a>
|
||||
</div>
|
||||
<div class="code-hero">
|
||||
<span class="comment">// Convert markdown to PDF in one call</span><br>
|
||||
<span class="key">curl</span> -X POST https://api.docfast.dev/v1/convert/markdown \<br>
|
||||
<span class="key">curl</span> -X POST https://docfast.dev/v1/convert/markdown \<br>
|
||||
-H <span class="string">"Authorization: Bearer YOUR_KEY"</span> \<br>
|
||||
-H <span class="string">"Content-Type: application/json"</span> \<br>
|
||||
-d <span class="string">'{"markdown": "# Invoice\\n\\nAmount: $500"}'</span> \<br>
|
||||
|
|
@ -139,6 +156,11 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0.
|
|||
<span class="endpoint-path">/v1/convert/markdown</span>
|
||||
<div class="endpoint-desc">Convert Markdown to styled PDF with syntax highlighting.</div>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span class="endpoint-method">POST</span>
|
||||
<span class="endpoint-path">/v1/convert/url</span>
|
||||
<div class="endpoint-desc">Navigate to a URL and convert the page to PDF.</div>
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span class="endpoint-method">GET</span>
|
||||
<span class="endpoint-path">/v1/templates</span>
|
||||
|
|
@ -171,7 +193,7 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0.
|
|||
<li>All templates</li>
|
||||
<li>Community support</li>
|
||||
</ul>
|
||||
<a href="mailto:hello@docfast.dev?subject=Free API Key" class="btn btn-secondary" style="width:100%">Get Free Key</a>
|
||||
<button class="btn btn-secondary" style="width:100%" onclick="openSignup()">Get Free Key</button>
|
||||
</div>
|
||||
<div class="price-card featured">
|
||||
<h3>Pro</h3>
|
||||
|
|
@ -183,7 +205,7 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0.
|
|||
<li>Priority support</li>
|
||||
<li>Custom templates</li>
|
||||
</ul>
|
||||
<a href="#" class="btn btn-primary" style="width:100%" onclick="checkout(event)">Get Started</a>
|
||||
<button class="btn btn-primary" style="width:100%" onclick="checkout()">Get Started</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -195,9 +217,102 @@ footer { padding: 40px 0; text-align: center; color: var(--muted); font-size: 0.
|
|||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Signup Modal -->
|
||||
<div class="modal-overlay" id="signupModal">
|
||||
<div class="modal" style="position:relative">
|
||||
<button class="close" onclick="closeSignup()">×</button>
|
||||
<div id="signupForm">
|
||||
<h2>Get Your Free API Key</h2>
|
||||
<p>Enter your email and get an API key instantly. No credit card required.</p>
|
||||
<div class="error" id="signupError"></div>
|
||||
<input type="email" id="signupEmail" placeholder="you@example.com" autofocus>
|
||||
<button class="btn btn-primary" style="width:100%" onclick="submitSignup()" id="signupBtn">Get API Key</button>
|
||||
</div>
|
||||
<div class="key-result" id="keyResult">
|
||||
<h2>🚀 You're in!</h2>
|
||||
<p>Here's your API key. <strong>Save it now</strong> — it won't be shown again.</p>
|
||||
<div class="key-box" id="apiKeyDisplay" onclick="copyKey()" title="Click to copy"></div>
|
||||
<div class="copy-hint">Click to copy</div>
|
||||
<p style="margin-top:24px;color:var(--muted);font-size:0.9rem;">100 free PDFs/month • All endpoints • <a href="#endpoints">View docs →</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function checkout(e) {
|
||||
e.preventDefault();
|
||||
function openSignup() {
|
||||
document.getElementById('signupModal').classList.add('active');
|
||||
document.getElementById('signupForm').style.display = 'block';
|
||||
document.getElementById('keyResult').style.display = 'none';
|
||||
document.getElementById('signupEmail').value = '';
|
||||
document.getElementById('signupError').style.display = 'none';
|
||||
setTimeout(() => document.getElementById('signupEmail').focus(), 100);
|
||||
}
|
||||
|
||||
function closeSignup() {
|
||||
document.getElementById('signupModal').classList.remove('active');
|
||||
}
|
||||
|
||||
// Close on overlay click
|
||||
document.getElementById('signupModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeSignup();
|
||||
});
|
||||
|
||||
// Submit on Enter
|
||||
document.getElementById('signupEmail').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') submitSignup();
|
||||
});
|
||||
|
||||
async function submitSignup() {
|
||||
const email = document.getElementById('signupEmail').value.trim();
|
||||
const errEl = document.getElementById('signupError');
|
||||
const btn = document.getElementById('signupBtn');
|
||||
|
||||
if (!email) {
|
||||
errEl.textContent = 'Please enter your email.';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.textContent = 'Creating...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/v1/signup/free', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
errEl.textContent = data.error || 'Something went wrong.';
|
||||
errEl.style.display = 'block';
|
||||
btn.textContent = 'Get API Key';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show key
|
||||
document.getElementById('signupForm').style.display = 'none';
|
||||
document.getElementById('keyResult').style.display = 'block';
|
||||
document.getElementById('apiKeyDisplay').textContent = data.apiKey;
|
||||
} catch (err) {
|
||||
errEl.textContent = 'Network error. Please try again.';
|
||||
errEl.style.display = 'block';
|
||||
btn.textContent = 'Get API Key';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function copyKey() {
|
||||
const key = document.getElementById('apiKeyDisplay').textContent;
|
||||
navigator.clipboard.writeText(key).then(() => {
|
||||
document.querySelector('.copy-hint').textContent = '✓ Copied!';
|
||||
setTimeout(() => document.querySelector('.copy-hint').textContent = 'Click to copy', 2000);
|
||||
});
|
||||
}
|
||||
|
||||
async function checkout() {
|
||||
try {
|
||||
const res = await fetch('/v1/billing/checkout', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue