Migrate from JSON to PostgreSQL, update SLA to 99.5%
- Replace JSON file storage with PostgreSQL (pg package) - Add db.ts service for connection pool and schema init - Rewrite keys.ts, verification.ts, usage.ts for async PostgreSQL - Update all routes for async function signatures - Add migration script (scripts/migrate-to-postgres.mjs) - Update docker-compose.yml with DATABASE_* env vars - Change SLA from 99.9% to 99.5% in landing page
This commit is contained in:
parent
bb1881af61
commit
e9d16bf2a3
13 changed files with 395 additions and 198 deletions
|
|
@ -1,11 +1,5 @@
|
|||
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");
|
||||
import pool from "./db.js";
|
||||
|
||||
export interface ApiKey {
|
||||
key: string;
|
||||
|
|
@ -15,47 +9,48 @@ export interface ApiKey {
|
|||
stripeCustomerId?: string;
|
||||
}
|
||||
|
||||
interface KeyStore {
|
||||
keys: ApiKey[];
|
||||
}
|
||||
// In-memory cache for fast lookups, synced with PostgreSQL
|
||||
let keysCache: ApiKey[] = [];
|
||||
|
||||
let store: KeyStore = { keys: [] };
|
||||
|
||||
function ensureDataDir(): void {
|
||||
if (!existsSync(DATA_DIR)) {
|
||||
mkdirSync(DATA_DIR, { recursive: true });
|
||||
export async function loadKeys(): Promise<void> {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
"SELECT key, tier, email, created_at, stripe_customer_id FROM api_keys"
|
||||
);
|
||||
keysCache = result.rows.map((r) => ({
|
||||
key: r.key,
|
||||
tier: r.tier as "free" | "pro",
|
||||
email: r.email,
|
||||
createdAt: r.created_at instanceof Date ? r.created_at.toISOString() : r.created_at,
|
||||
stripeCustomerId: r.stripe_customer_id || undefined,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error("Failed to load keys from PostgreSQL:", err);
|
||||
keysCache = [];
|
||||
}
|
||||
}
|
||||
|
||||
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() });
|
||||
if (!keysCache.find((e) => e.key === k)) {
|
||||
const entry: ApiKey = { key: k, tier: "pro", email: "seed@docfast.dev", createdAt: new Date().toISOString() };
|
||||
keysCache.push(entry);
|
||||
// Upsert into DB
|
||||
await pool.query(
|
||||
`INSERT INTO api_keys (key, tier, email, created_at) VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (key) DO NOTHING`,
|
||||
[k, "pro", "seed@docfast.dev", new Date().toISOString()]
|
||||
).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return keysCache.some((k) => k.key === key);
|
||||
}
|
||||
|
||||
export function getKeyInfo(key: string): ApiKey | undefined {
|
||||
return store.keys.find((k) => k.key === key);
|
||||
return keysCache.find((k) => k.key === key);
|
||||
}
|
||||
|
||||
export function isProKey(key: string): boolean {
|
||||
|
|
@ -67,10 +62,9 @@ function generateKey(prefix: string): string {
|
|||
return `${prefix}_${randomBytes(24).toString("hex")}`;
|
||||
}
|
||||
|
||||
export function createFreeKey(email?: string): ApiKey {
|
||||
// If email provided, check if it already has a free key
|
||||
export async function createFreeKey(email?: string): Promise<ApiKey> {
|
||||
if (email) {
|
||||
const existing = store.keys.find((k) => k.email === email && k.tier === "free");
|
||||
const existing = keysCache.find((k) => k.email === email && k.tier === "free");
|
||||
if (existing) return existing;
|
||||
}
|
||||
|
||||
|
|
@ -80,16 +74,20 @@ export function createFreeKey(email?: string): ApiKey {
|
|||
email: email || "",
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
store.keys.push(entry);
|
||||
save();
|
||||
|
||||
await pool.query(
|
||||
"INSERT INTO api_keys (key, tier, email, created_at) VALUES ($1, $2, $3, $4)",
|
||||
[entry.key, entry.tier, entry.email, entry.createdAt]
|
||||
);
|
||||
keysCache.push(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function createProKey(email: string, stripeCustomerId: string): ApiKey {
|
||||
const existing = store.keys.find((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
export async function createProKey(email: string, stripeCustomerId: string): Promise<ApiKey> {
|
||||
const existing = keysCache.find((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
if (existing) {
|
||||
existing.tier = "pro";
|
||||
save();
|
||||
await pool.query("UPDATE api_keys SET tier = 'pro' WHERE key = $1", [existing.key]);
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
|
@ -100,29 +98,34 @@ export function createProKey(email: string, stripeCustomerId: string): ApiKey {
|
|||
createdAt: new Date().toISOString(),
|
||||
stripeCustomerId,
|
||||
};
|
||||
store.keys.push(entry);
|
||||
save();
|
||||
|
||||
await pool.query(
|
||||
"INSERT INTO api_keys (key, tier, email, created_at, stripe_customer_id) VALUES ($1, $2, $3, $4, $5)",
|
||||
[entry.key, entry.tier, entry.email, entry.createdAt, entry.stripeCustomerId]
|
||||
);
|
||||
keysCache.push(entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
export function revokeByCustomer(stripeCustomerId: string): boolean {
|
||||
const idx = store.keys.findIndex((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
export async function revokeByCustomer(stripeCustomerId: string): Promise<boolean> {
|
||||
const idx = keysCache.findIndex((k) => k.stripeCustomerId === stripeCustomerId);
|
||||
if (idx >= 0) {
|
||||
store.keys.splice(idx, 1);
|
||||
save();
|
||||
const key = keysCache[idx].key;
|
||||
keysCache.splice(idx, 1);
|
||||
await pool.query("DELETE FROM api_keys WHERE key = $1", [key]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getAllKeys(): ApiKey[] {
|
||||
return [...store.keys];
|
||||
return [...keysCache];
|
||||
}
|
||||
|
||||
export function updateKeyEmail(apiKey: string, newEmail: string): boolean {
|
||||
const entry = store.keys.find(k => k.key === apiKey);
|
||||
export async function updateKeyEmail(apiKey: string, newEmail: string): Promise<boolean> {
|
||||
const entry = keysCache.find((k) => k.key === apiKey);
|
||||
if (!entry) return false;
|
||||
entry.email = newEmail;
|
||||
save();
|
||||
await pool.query("UPDATE api_keys SET email = $1 WHERE key = $2", [newEmail, apiKey]);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue