117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
import express from "express";
|
|
import helmet from "helmet";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
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 { 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({ crossOriginResourcePolicy: { policy: "cross-origin" } }));
|
|
|
|
// CORS — allow browser requests from the landing page
|
|
app.use((req, res, next) => {
|
|
const origin = req.headers.origin;
|
|
const allowed = ["https://docfast.dev", "http://localhost:3100"];
|
|
if (origin && allowed.includes(origin)) {
|
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
}
|
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
|
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
if (req.method === "OPTIONS") {
|
|
res.status(204).end();
|
|
return;
|
|
}
|
|
next();
|
|
});
|
|
// 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/*" }));
|
|
|
|
// Trust nginx proxy
|
|
app.set("trust proxy", 1);
|
|
|
|
// Rate limiting
|
|
const limiter = rateLimit({
|
|
windowMs: 60_000,
|
|
max: 100,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
});
|
|
app.use(limiter);
|
|
|
|
// Public routes
|
|
app.use("/health", healthRouter);
|
|
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);
|
|
|
|
// 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")));
|
|
|
|
// Docs page (clean URL)
|
|
app.get("/docs", (_req, res) => {
|
|
res.sendFile(path.join(__dirname, "../public/docs.html"));
|
|
});
|
|
|
|
// API root
|
|
app.get("/api", (_req, res) => {
|
|
res.json({
|
|
name: "DocFast API",
|
|
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...");
|
|
await closeBrowser();
|
|
process.exit(0);
|
|
};
|
|
process.on("SIGTERM", shutdown);
|
|
process.on("SIGINT", shutdown);
|
|
}
|
|
|
|
start().catch((err) => {
|
|
console.error("Failed to start:", err);
|
|
process.exit(1);
|
|
});
|
|
|
|
export { app };
|