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
339 changes: 339 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

94 changes: 78 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { disconnectRedis } from "./lib/redis";
import { initSocket } from "./lib/socket";
import { SorobanEventListener } from "./services/sorobanEventListener";
import { multiSigSubmissionService } from "./services/multiSigSubmissionService";
import { apiKeyMiddleware } from "./middleware/apiKeyMiddleware";
import logger from "./utils/logger";
import { GasBalanceMonitorService, getGasBalanceMonitorService } from "./services/gasBalanceMonitorService";
import {
GasBalanceMonitorService,
getGasBalanceMonitorService,
Expand Down Expand Up @@ -67,9 +70,9 @@ for (const envVar of requiredEnvVars) {
}

if (missingEnvVars.length > 0) {
console.error("❌ Missing required environment variables:");
missingEnvVars.forEach((varName) => console.error(` - ${varName}`));
console.error(
logger.error("❌ Missing required environment variables:");
missingEnvVars.forEach((varName) => logger.error(` - ${varName}`));
logger.error(
"\nPlease set these variables in your .env file and restart the server.",
);
process.exit(1);
Expand All @@ -81,7 +84,7 @@ const dashboardUrl =
"http://localhost:3000";

if (!dashboardUrl) {
console.error("❌ Missing required environment variable: DASHBOARD_URL");
logger.error("❌ Missing required environment variable: DASHBOARD_URL");
process.exit(1);
}

Expand All @@ -96,14 +99,61 @@ const horizonUrl =
const horizonServer = new Horizon.Server(horizonUrl);

// Middleware
app.use(cors());
app.use(
morgan(":method :url :status :res[content-length] - :response-time ms", {
stream: {
write: (message) => logger.http(message.trim()),
},
}),
);
app.use(
cors({
origin: (origin, callback) => {
// Allow non-browser requests (e.g. curl, server-to-server)
if (!origin) {
return callback(null, true);
}

if (origin === dashboardUrl) {
return callback(null, true);
}

return callback(
new Error(
`CORS policy: Access denied from origin ${origin}. Allowed origin: ${dashboardUrl}`,
),
);
},
credentials: true,
}),
);
app.use(express.json());

// Swagger documentation
app.use("/api/docs", swaggerUi.serve);
app.get(
"/api/docs",
swaggerUi.setup(specs, {
swaggerOptions: {
persistAuthorization: true,
},
customCss: `
.topbar { display: none; }
.swagger-ui .api-info { margin-bottom: 20px; }
`,
customSiteTitle: "StellarFlow API Documentation",
}),
);
// Apply API Key Middleware to all /api routes
app.use("/api", apiKeyMiddleware);

// Routes
app.use("/api/market-rates", marketRatesRouter);
app.use("/api/history", historyRouter);
app.use("/api/price-updates", priceUpdatesRouter);
app.use("/api/stats", statsRouter);
app.use("/api/intelligence", intelligenceRouter);
app.use("/api/price-updates", priceUpdatesRouter);
app.use("/api/assets", assetsRouter);

// Health check endpoint
/**
Expand Down Expand Up @@ -238,6 +288,18 @@ app.get("/", (req, res) => {
});
});

// Error handling middleware
app.use(
(
err: Error,
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
logger.error(`Unhandled error: ${err.message}`, { stack: err.stack });
res.status(500).json({
success: false,
error: "Internal server error",
// Start server
const httpServer = createServer(app);
initSocket(httpServer);
Expand Down Expand Up @@ -329,25 +391,25 @@ process.once("SIGTERM", () => {
});

httpServer.listen(PORT, () => {
console.log(`🌊 StellarFlow Backend running on port ${PORT}`);
console.log(
logger.info(`🌊 StellarFlow Backend running on port ${PORT}`);
logger.info(
`📊 Market Rates API available at http://localhost:${PORT}/api/market-rates`,
);
console.log(
logger.info(
`📚 API Documentation available at http://localhost:${PORT}/api/docs`,
);
console.log(`🏥 Health check at http://localhost:${PORT}/health`);
console.log(`🔌 Socket.io ready for dashboard connections`);
logger.info(`🏥 Health check at http://localhost:${PORT}/health`);
logger.info(`🔌 Socket.io ready for dashboard connections`);

// Start Soroban event listener to track confirmed on-chain prices
try {
sorobanEventListener = new SorobanEventListener();
sorobanEventListener.start().catch((err) => {
console.error("Failed to start event listener:", err);
});
console.log(`👂 Soroban event listener started`);
logger.info(`👂 Soroban event listener started`);
} catch (err) {
console.warn(
logger.warn(
"Event listener not started:",
err instanceof Error ? err.message : err,
);
Expand All @@ -358,11 +420,11 @@ httpServer.listen(PORT, () => {
if (process.env.MULTI_SIG_ENABLED === "true") {
try {
multiSigSubmissionService.start().catch((err: Error) => {
console.error("Failed to start multi-sig submission service:", err);
logger.error("Failed to start multi-sig submission service:", err);
});
console.log(`🔐 Multi-Sig submission service started`);
logger.info(`🔐 Multi-Sig submission service started`);
} catch (err) {
console.warn(
logger.warn(
"Multi-sig submission service not started:",
err instanceof Error ? err.message : err,
);
Expand Down
5 changes: 5 additions & 0 deletions src/lib/socket.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Server, Socket } from "socket.io";
import { randomUUID } from "crypto";
import logger from "../utils/logger";

interface Session {
id: string; // connectionSessionId
Expand Down Expand Up @@ -44,6 +45,10 @@ export function initSocket(server: import("http").Server): Server {
pingTimeout: HEARTBEAT_TIMEOUT,
});

io.on("connection", (socket) => {
logger.info(`🔌 Client connected: ${socket.id}`);
socket.on("disconnect", () =>
logger.info(`🔌 Client disconnected: ${socket.id}`)
io.on("connection", (socket: Socket) => {
console.log(`🔌 Client connected: ${socket.id}`);

Expand Down
72 changes: 71 additions & 1 deletion src/middleware/apiKeyMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import crypto from "crypto";
import { Request, Response, NextFunction } from "express";
import logger from "../utils/logger";
import prisma from "../lib/prisma";
import {
ApiScope,
Expand All @@ -8,6 +8,76 @@ import {
requiredScopeForMethod,
} from "../types/apiKey.types";

export const apiKeyMiddleware = async (
req: Request,
res: Response,
next: NextFunction,
): Promise<void> => {
// Short-circuit if already authenticated by a previous middleware instance
if (req.relayer) {
next();
return;
}

const apiKey = req.headers["x-api-key"];

if (typeof apiKey !== "string" || apiKey.length === 0) {
res.status(401).json({
success: false,
error: "Invalid or missing API key",
});
return;
}

try {
// 1. Try to find an active relayer with this API key
const relayer = await prisma.relayer.findFirst({
where: {
apiKey,
isActive: true,
},
});

if (relayer) {
req.relayer = {
id: relayer.id,
name: relayer.name,
allowedAssets: relayer.allowedAssets,
publicKey: relayer.publicKey,
};
next();
return;
}

// 2. Fall back to global API key for backward compatibility
const expectedKey = process.env.API_KEY;

if (!expectedKey) {
logger.error("Critical: API_KEY not set in environment");
return res.status(500).json({
success: false,
error: "Authentication configuration error",
});
}

if (apiKey === expectedKey) {
next();
return;
}

res.status(401).json({
success: false,
error: "Invalid or missing API key",
});
} catch (error) {
console.error("[apiKeyMiddleware] Error during authentication:", error);
res.status(500).json({
success: false,
error: "Authentication check failed",
});
}
};

// ------------------------------------------------------------------
// Extend Express's Request so downstream handlers can safely access
// req.apiKey without casting.
Expand Down
3 changes: 3 additions & 0 deletions src/routes/marketRates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Router } from "express";
import { getRate, getAllRates } from "../controllers/marketRatesController";
import { MarketRateService } from "../services/marketRate";
import logger from "../utils/logger";
import { cacheMiddleware, invalidateCache } from "../cache/CacheMiddleware";
import { CACHE_CONFIG, CACHE_KEYS } from "../config/redis.config";
import { isLockdownError } from "../state/appState";
Expand Down Expand Up @@ -91,6 +92,8 @@ router.get(
: "Failed to fetch pending price reviews",
});
}
} catch (error) {
logger.error("Error fetching latest prices:", error);
},
);

Expand Down
Loading