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
118
src/services/keys.ts
Normal file
118
src/services/keys.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { randomBytes } from "crypto";
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DATA_DIR = path.join(__dirname, "../../data");
|
||||
const KEYS_FILE = path.join(DATA_DIR, "keys.json");
|
||||
|
||||
export interface ApiKey {
|
||||
key: string;
|
||||
tier: "free" | "pro";
|
||||
email: string;
|
||||
createdAt: string;
|
||||
stripeCustomerId?: string;
|
||||
}
|
||||
|
||||
interface KeyStore {
|
||||
keys: ApiKey[];
|
||||
}
|
||||
|
||||
let store: KeyStore = { keys: [] };
|
||||
|
||||
function ensureDataDir(): void {
|
||||
if (!existsSync(DATA_DIR)) {
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function loadKeys(): void {
|
||||
ensureDataDir();
|
||||
if (existsSync(KEYS_FILE)) {
|
||||
try {
|
||||
store = JSON.parse(readFileSync(KEYS_FILE, "utf-8"));
|
||||
} catch {
|
||||
store = { keys: [] };
|
||||
}
|
||||
}
|
||||
// Also load seed keys from env
|
||||
const envKeys = process.env.API_KEYS?.split(",").map((k) => k.trim()).filter(Boolean) || [];
|
||||
for (const k of envKeys) {
|
||||
if (!store.keys.find((e) => e.key === k)) {
|
||||
store.keys.push({ key: k, tier: "pro", email: "seed@docfast.dev", createdAt: new Date().toISOString() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
ensureDataDir();
|
||||
writeFileSync(KEYS_FILE, JSON.stringify(store, null, 2));
|
||||
}
|
||||
|
||||
export function isValidKey(key: string): boolean {
|
||||
return store.keys.some((k) => k.key === key);
|
||||
}
|
||||
|
||||
export function getKeyInfo(key: string): ApiKey | undefined {
|
||||
return store.keys.find((k) => k.key === key);
|
||||
}
|
||||
|
||||
export function isProKey(key: string): boolean {
|
||||
const info = getKeyInfo(key);
|
||||
return info?.tier === "pro";
|
||||
}
|
||||
|
||||
function generateKey(prefix: string): string {
|
||||
return `${prefix}_${randomBytes(24).toString("hex")}`;
|
||||
}
|
||||
|
||||
export function createFreeKey(email: string): ApiKey {
|
||||
// Check if email already has a free key
|
||||
const existing = store.keys.find((k) => k.email === email && k.tier === "free");
|
||||
if (existing) return existing;
|
||||
|
||||
const entry: ApiKey = {
|
||||
key: generateKey("df_free"),
|
||||
tier: "free",
|
||||
email,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
store.keys.push(entry);
|
||||
save();
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function createProKey(email: string, stripeCustomerId: string): ApiKey {
|
||||
const existing = store.keys.find((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
if (existing) {
|
||||
existing.tier = "pro";
|
||||
save();
|
||||
return existing;
|
||||
}
|
||||
|
||||
const entry: ApiKey = {
|
||||
key: generateKey("df_pro"),
|
||||
tier: "pro",
|
||||
email,
|
||||
createdAt: new Date().toISOString(),
|
||||
stripeCustomerId,
|
||||
};
|
||||
store.keys.push(entry);
|
||||
save();
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function revokeByCustomer(stripeCustomerId: string): boolean {
|
||||
const idx = store.keys.findIndex((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
if (idx >= 0) {
|
||||
store.keys.splice(idx, 1);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getAllKeys(): ApiKey[] {
|
||||
return [...store.keys];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue