203 lines
7.3 KiB
TypeScript
203 lines
7.3 KiB
TypeScript
import type { Express, Request, Response } from "express";
|
|
import { createServer } from "node:http";
|
|
import type { Server } from "node:http";
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
import * as session from "express-session";
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
import MemoryStoreModule from "memorystore";
|
|
const MemoryStore = MemoryStoreModule(session.default || session);
|
|
import {
|
|
SYMBOLS,
|
|
computeExpirations as mockExpirations,
|
|
computeGexProfile as mockGexProfile,
|
|
computeScreener as mockScreener,
|
|
computeSummary as mockSummary,
|
|
} from "./marketData";
|
|
import { oratsClient } from "./oratsClient";
|
|
import { registerAuthRoutes } from "./authRoutes";
|
|
import { initDb } from "./db";
|
|
import { rateLimit, securityHeaders } from "./middleware";
|
|
|
|
const SessionMemoryStore = new MemoryStore({ checkPeriod: 86400000 });
|
|
|
|
const KNOWN_TICKERS = new Set(SYMBOLS.map((s) => s.ticker));
|
|
|
|
function withTicker(req: Request, res: Response, fn: (ticker: string) => unknown) {
|
|
const raw = String(req.params.symbol || "").toUpperCase();
|
|
if (!KNOWN_TICKERS.has(raw)) {
|
|
res.status(404).json({ error: `Unknown symbol: ${raw}` });
|
|
return;
|
|
}
|
|
try {
|
|
res.json(fn(raw));
|
|
} catch (err) {
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
}
|
|
|
|
export async function registerRoutes(httpServer: Server, app: Express): Promise<Server> {
|
|
await initDb();
|
|
|
|
// ── Security Headers ──────────────────────────────────────────────
|
|
app.use(securityHeaders);
|
|
|
|
// ── Rate Limiting ─────────────────────────────────────────────────
|
|
// General rate limit: 500 requests per 15 minutes (relaxed for dev)
|
|
// Home subnet (192.168.x.x) is whitelisted in middleware.ts
|
|
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 500 }));
|
|
|
|
// Auth-specific rate limit: 50 requests per 15 minutes (relaxed for dev)
|
|
app.use("/api/auth/", rateLimit({ windowMs: 15 * 60 * 1000, max: 50 }));
|
|
|
|
// ── Session Configuration ─────────────────────────────────────────
|
|
app.use(
|
|
(session.default || session)({
|
|
store: new MemoryStore({ checkPeriod: 86400000 }),
|
|
secret: process.env.SESSION_SECRET || "gammadesk-dev-secret-change-me",
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
secure: process.env.NODE_ENV === "production",
|
|
httpOnly: true, // Prevent XSS from accessing session cookie
|
|
sameSite: "strict", // Prevent CSRF
|
|
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
},
|
|
}),
|
|
);
|
|
|
|
// ── Body Parsing ──────────────────────────────────────────────────
|
|
app.use(express.json({ limit: "1mb" }));
|
|
app.use(express.urlencoded({ extended: false, limit: "1mb" }));
|
|
|
|
// Auth routes
|
|
registerAuthRoutes(app);
|
|
|
|
// Public API routes
|
|
app.get("/api/symbols", (_req, res) => {
|
|
res.json(SYMBOLS);
|
|
});
|
|
|
|
// ORATS status - hide sensitive info
|
|
app.get("/api/orats/status", (_req, res) => {
|
|
res.json({
|
|
configured: oratsClient.isConfigured(),
|
|
baseUrl: oratsClient.isConfigured() ? "https://api.orats.io/datav2" : "not-configured",
|
|
});
|
|
});
|
|
|
|
// Save ORATS API key + base URL to session
|
|
app.post("/api/orats/config", (req, res) => {
|
|
const { apiKey, baseUrl } = req.body as Record<string, string>;
|
|
const session = req.session as Record<string, unknown>;
|
|
if (!apiKey || typeof apiKey !== "string") {
|
|
return res.status(400).json({ error: "API key is required" });
|
|
}
|
|
const sanitizedApiKey = apiKey.trim().replace(/[<>]/g, "");
|
|
session.oratsApiKey = sanitizedApiKey;
|
|
// Update the orats client with the new key
|
|
oratsClient.setApiKey(sanitizedApiKey);
|
|
if (baseUrl && typeof baseUrl === "string") {
|
|
oratsClient.setBaseUrl(baseUrl);
|
|
}
|
|
res.json({ ok: true, configured: oratsClient.isConfigured() });
|
|
});
|
|
|
|
// Clear ORATS API key from session
|
|
app.delete("/api/orats/config", (req, res) => {
|
|
const session = req.session as Record<string, unknown>;
|
|
delete session.oratsApiKey;
|
|
oratsClient.setApiKey("");
|
|
res.json({ ok: true, configured: oratsClient.isConfigured() });
|
|
});
|
|
|
|
// Market data routes
|
|
app.get("/api/market/:symbol/summary", async (req, res) => {
|
|
const ticker = String(req.params.symbol || "").toUpperCase();
|
|
if (!KNOWN_TICKERS.has(ticker)) {
|
|
return res.status(404).json({ error: `Unknown symbol: ${ticker}` });
|
|
}
|
|
try {
|
|
const summary = oratsClient.isConfigured()
|
|
? await oratsClient.computeSummary(ticker)
|
|
: mockSummary(ticker);
|
|
res.json(summary);
|
|
} catch (err) {
|
|
console.error(`ORATS summary error for ${ticker}:`, err);
|
|
res.json(mockSummary(ticker));
|
|
}
|
|
});
|
|
|
|
app.get("/api/market/:symbol/gex", async (req, res) => {
|
|
const ticker = String(req.params.symbol || "").toUpperCase();
|
|
if (!KNOWN_TICKERS.has(ticker)) {
|
|
return res.status(404).json({ error: `Unknown symbol: ${ticker}` });
|
|
}
|
|
try {
|
|
const gex = oratsClient.isConfigured()
|
|
? await oratsClient.computeGexProfile(ticker)
|
|
: mockGexProfile(ticker);
|
|
res.json(gex);
|
|
} catch (err) {
|
|
console.error(`ORATS GEX error for ${ticker}:`, err);
|
|
res.json(mockGexProfile(ticker));
|
|
}
|
|
});
|
|
|
|
app.get("/api/market/:symbol/expirations", async (req, res) => {
|
|
const ticker = String(req.params.symbol || "").toUpperCase();
|
|
if (!KNOWN_TICKERS.has(ticker)) {
|
|
return res.status(404).json({ error: `Unknown symbol: ${ticker}` });
|
|
}
|
|
try {
|
|
const exp = oratsClient.isConfigured()
|
|
? await oratsClient.computeExpirations(ticker)
|
|
: mockExpirations(ticker);
|
|
res.json(exp);
|
|
} catch (err) {
|
|
console.error(`ORATS expirations error for ${ticker}:`, err);
|
|
res.json(mockExpirations(ticker));
|
|
}
|
|
});
|
|
|
|
app.get("/api/screener", async (_req, res) => {
|
|
try {
|
|
if (oratsClient.isConfigured()) {
|
|
const summaries = await Promise.all(
|
|
SYMBOLS.map((s) => oratsClient.computeSummary(s.ticker)),
|
|
);
|
|
const screenerData = summaries.map((summary, i) => {
|
|
const symbol = SYMBOLS[i];
|
|
return {
|
|
ticker: symbol.ticker,
|
|
name: symbol.name,
|
|
spot: summary.spot,
|
|
spotChangePct: summary.spotChangePct,
|
|
netGex: summary.netGex,
|
|
gammaRegime: summary.gammaRegime,
|
|
ivRank: summary.ivRank,
|
|
ivx: summary.ivx,
|
|
callWall: summary.callWall,
|
|
putWall: summary.putWall,
|
|
distanceToCallWall: ((summary.callWall - summary.spot) / summary.spot) * 100,
|
|
distanceToPutWall: ((summary.putWall - summary.spot) / summary.spot) * 100,
|
|
expectedMovePct: summary.expectedMovePct,
|
|
skew: summary.skew,
|
|
zeroDteNetGex: summary.netGex,
|
|
};
|
|
});
|
|
res.json(screenerData);
|
|
} else {
|
|
res.json(mockScreener());
|
|
}
|
|
} catch (err) {
|
|
console.error("ORATS screener error:", err);
|
|
res.json(mockScreener());
|
|
}
|
|
});
|
|
|
|
return httpServer;
|
|
}
|
|
|
|
// Import express for body parsing
|
|
import express from "express";
|