Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 42 additions & 38 deletions backend/dist/config/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,47 +156,51 @@ async function connectWithRetry() {
// ---------------------------------------------------------------------------
const adapter = new adapter_pg_1.PrismaPg(exports.pool);
const globalForPrisma = global;
// Initialize Prisma with optimized middleware for tracing and performance monitoring
exports.prisma = globalForPrisma.prisma ||
new client_1.PrismaClient({
function createPrismaClient() {
const client = new client_1.PrismaClient({
adapter,
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
// Add query middleware for tracing and performance monitoring
exports.prisma.$use(async (params, next) => {
const spanContext = tracing_1.context.active();
const startTime = Date.now();
const logger = tracing_1.trace.getLogger("db-query");
try {
const result = await next(params);
const duration = Date.now() - startTime;
// Log slow queries (> 1000ms)
if (duration > 1000) {
logger.warn(`Slow query detected: ${params.model}.${params.action}`, {
duration,
model: params.model,
action: params.action,
args: JSON.stringify(params.args).substring(0, 200),
});
}
logger.debug(`Query completed: ${params.model}.${params.action}`, {
duration,
model: params.model,
action: params.action,
});
return result;
}
catch (error) {
const duration = Date.now() - startTime;
logger.error(`Query failed: ${params.model}.${params.action}`, {
duration,
model: params.model,
action: params.action,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
});
return client.$extends({
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
const startTime = Date.now();
const logger = tracing_1.trace.getLogger("db-query");
try {
const result = await query(args);
const duration = Date.now() - startTime;
if (duration > 1000) {
logger.warn(`Slow query detected: ${model}.${operation}`, {
duration,
model,
action: operation,
args: JSON.stringify(args).substring(0, 200),
});
}
logger.debug(`Query completed: ${model}.${operation}`, {
duration,
model,
action: operation,
});
return result;
}
catch (error) {
const duration = Date.now() - startTime;
logger.error(`Query failed: ${model}.${operation}`, {
duration,
model,
action: operation,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
},
},
},
});
}
exports.prisma = globalForPrisma.prisma || createPrismaClient();
if (process.env.NODE_ENV !== "production")
globalForPrisma.prisma = exports.prisma;
// ---------------------------------------------------------------------------
Expand Down
36 changes: 36 additions & 0 deletions backend/dist/config/redis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use strict";
/**
* src/config/redis.ts
*
* Singleton ioredis client shared across the entire backend.
*
* Used by:
* • JWT blacklist (auth routes — BE-W3A-105)
* • Refresh-token revocation lookups
* • Future: distributed rate-limiting, caching
*
* Design decisions:
* • `lazyConnect: true` — the server boots even if Redis is momentarily
* unavailable; the health endpoint reports degraded status instead of
* crashing the process.
* • Exponential retry capped at 10 s so transient Redis restarts self-heal
* without flooding logs.
* • `keepAlive: 5000` prevents silent TCP drops behind cloud load-balancers.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.redis = void 0;
const ioredis_1 = __importDefault(require("ioredis"));
const REDIS_URL = process.env.REDIS_URL || "redis://localhost:6379";
exports.redis = new ioredis_1.default(REDIS_URL, {
// Exponential back-off: 100 ms → 200 ms → … capped at 10 s
retryStrategy: (times) => Math.min(times * 100, 10_000),
lazyConnect: true,
keepAlive: 5_000,
// Name shown in Redis CLIENT LIST — useful when debugging multi-service setups
connectionName: "lance-backend",
});
exports.redis.on("connect", () => console.log("[redis] connected to", REDIS_URL.replace(/:\/\/.*@/, "://<credentials>@")));
exports.redis.on("error", (err) => console.error("[redis] error:", err.message));
83 changes: 78 additions & 5 deletions backend/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const cookie_parser_1 = __importDefault(require("cookie-parser"));
const crypto_1 = __importDefault(require("crypto"));
const dotenv_1 = __importDefault(require("dotenv"));
const db_1 = require("./config/db");
const tracing_1 = require("./utils/tracing");
const tracing_1 = require("./config/tracing");
const intakeRateLimit_1 = require("./middleware/intakeRateLimit");
const sanitize_1 = require("./middleware/sanitize");
const tracing_2 = require("./utils/tracing");
const metrics_1 = require("./middleware/metrics");
const metrics_2 = require("./utils/metrics");
const auth_1 = __importDefault(require("./routes/auth"));
const jobs_1 = __importDefault(require("./routes/jobs"));
const disputes_1 = __importDefault(require("./routes/disputes"));
Expand All @@ -18,14 +25,73 @@ const uploads_1 = __importDefault(require("./routes/uploads"));
const bulk_1 = __importDefault(require("./routes/bulk"));
const pool_1 = __importDefault(require("./routes/pool"));
const state_1 = __importDefault(require("./routes/state"));
const db_2 = require("./config/db");
dotenv_1.default.config();
const app = (0, express_1.default)();
const port = process.env.PORT || 3001;
const logger = tracing_1.trace.getLogger("server");
// Enable CORS for frontend requests
app.use((0, cors_1.default)({ origin: "*" }));
const isProduction = process.env.NODE_ENV === "production";
const CSRF_COOKIE_NAME = "lance-csrf-token";
// Enable CORS for frontend requests with credentials support
const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:3000";
app.use((0, cors_1.default)({
origin: FRONTEND_URL,
credentials: true,
}));
app.use((0, cookie_parser_1.default)());
app.use(express_1.default.json());
app.use(tracing_1.tracingMiddleware); // Global request tracing and diagnostics
// CSRF protection middleware (double-submit cookie pattern)
const csrfMiddleware = (req, res, next) => {
// Skip CSRF for GET/HEAD/OPTIONS and auth challenge/verify routes
if (["GET", "HEAD", "OPTIONS"].includes(req.method) ||
(req.path.startsWith("/api/v1/auth/") &&
(req.path.endsWith("/challenge") || req.path.endsWith("/verify")))) {
return next();
}
const csrfCookie = req.cookies[CSRF_COOKIE_NAME];
const csrfHeader = req.headers["x-csrf-token"];
const csrfHeaderStr = Array.isArray(csrfHeader) ? csrfHeader[0] : csrfHeader;
if (!csrfCookie || !csrfHeaderStr || !crypto_1.default.timingSafeEqual(Buffer.from(csrfCookie), Buffer.from(csrfHeaderStr))) {
return res.status(403).json({ error: "Invalid CSRF token" });
}
next();
};
// Route to get CSRF token
app.get("/api/v1/auth/csrf", (req, res) => {
const csrfToken = crypto_1.default.randomBytes(32).toString("hex");
// Set CSRF cookie (HttpOnly false so frontend can read it, SameSite strict)
res.cookie(CSRF_COOKIE_NAME, csrfToken, {
httpOnly: false,
secure: isProduction,
sameSite: isProduction ? "strict" : "lax",
path: "/",
});
res.json({ csrfToken });
});
app.use(csrfMiddleware);
app.use(tracing_2.tracingMiddleware); // Global request tracing and diagnostics
app.use(intakeRateLimit_1.intakeRateLimit);
app.use(metrics_1.metricsMiddleware);
// SQL injection protection — inspects query params and body for injection patterns
app.use(sanitize_1.sqlInjectionGuard);
// Request logging middleware with tracing
app.use((req, res, next) => {
const startTime = Date.now();
const requestLogger = tracing_1.trace.getLogger(`http-${req.method}`);
res.on("finish", () => {
const duration = Date.now() - startTime;
const status = res.statusCode;
const statusCategory = status < 400 ? "success" : status < 500 ? "client_error" : "server_error";
requestLogger.info(`${req.method} ${req.path}`, {
method: req.method,
path: req.path,
status,
duration,
statusCategory,
});
});
next();
});
// Mount API routes
app.use("/api/v1/auth", auth_1.default);
app.use("/api/v1/jobs", jobs_1.default);
Expand All @@ -37,7 +103,8 @@ app.use("/api/v1/uploads", uploads_1.default);
app.use("/api/v1/bulk", bulk_1.default);
app.use("/api/v1/pool", pool_1.default);
app.use("/api/v1/state", state_1.default);
// Basic healthcheck route
app.use("/api/v1/metrics", (0, metrics_2.createMetricsRouter)());
// Health check endpoint with database connectivity verification
app.get("/health", async (req, res) => {
const startTime = Date.now();
logger.debug("Health check requested");
Expand Down Expand Up @@ -74,6 +141,7 @@ app.get("/health", async (req, res) => {
// Graceful shutdown handler
process.on("SIGTERM", async () => {
logger.info("SIGTERM received, shutting down gracefully");
stopStorageCleanup();
try {
await db_1.prisma.$disconnect();
logger.info("Database connection closed");
Expand All @@ -94,8 +162,13 @@ async function bootstrap() {
try {
await (0, db_1.connectWithRetry)();
(0, db_1.startPoolHealthCheck)();
startStorageCleanup();
app.listen(port, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
// Update pool metrics periodically so the Prometheus scrape has fresh data
setInterval(() => {
(0, metrics_2.updatePoolMetrics)(db_2.pool.totalCount, db_2.pool.idleCount, db_2.pool.waitingCount);
}, 15_000).unref();
});
}
catch (err) {
Expand Down
72 changes: 72 additions & 0 deletions backend/dist/middleware/authGuard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use strict";
/**
* src/middleware/authGuard.ts
*
* Express middleware that validates JWT access tokens on every protected route.
*
* Steps
* ─────
* 1. Extract Bearer token from Authorization header
* 2. Cryptographic signature + expiry check (jsonwebtoken)
* 3. Issuer / audience claim validation
* 4. Redis blacklist lookup for revoked `jti` values ← sub-ms, O(1)
*
* Usage
* ─────
* import { authGuard } from "../middleware/authGuard";
*
* // Protect a single route
* router.get("/profile", authGuard, profileHandler);
*
* // Protect an entire router
* app.use("/api/v1/jobs", authGuard, jobsRouter);
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authGuard = authGuard;
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const auth_1 = require("../routes/auth");
const ACCESS_TOKEN_COOKIE = "lance_access_token";
async function authGuard(req, res, next) {
// Try to get token from cookie first, then from Authorization header
let token = req.cookies[ACCESS_TOKEN_COOKIE];
const header = req.headers.authorization;
if (!token && header?.startsWith("Bearer ")) {
token = header.slice(7);
}
if (!token) {
res.status(401).json({ error: "Authorization token missing or malformed" });
return;
}
const secret = process.env.JWT_SECRET;
if (!secret) {
console.error("[authGuard] JWT_SECRET is not set");
res.status(500).json({ error: "Server misconfiguration" });
return;
}
let decoded;
try {
decoded = jsonwebtoken_1.default.verify(token, secret, {
issuer: "lance-marketplace",
audience: "lance-frontend",
});
}
catch {
res.status(401).json({ error: "Invalid or expired access token" });
return;
}
if (!decoded.jti) {
res.status(401).json({ error: "Token missing jti claim" });
return;
}
// Redis blacklist check — single GET, O(1), target < 1 ms.
const revoked = await (0, auth_1.isTokenBlacklisted)(decoded.jti).catch(() => false);
if (revoked) {
res.status(401).json({ error: "Token has been revoked" });
return;
}
req.auth = decoded;
next();
}
Loading
Loading