Some checks failed
Code Quality / SonarQube Analysis (pull_request) Failing after 26s
Code Quality / Code Quality Checks (pull_request) Failing after 6s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 3s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 3s
Adds a narrow, safelisted BFF surface so the Solace Bank Group PLC portal (and other browser clients) can reach the Cloudflare Access protected Proxmox API without requiring the user to complete a CF-Access SSO flow in-browser. Endpoints: GET /api/proxmox/health — configuration probe (503 when unset) GET /api/proxmox/cluster/status — aggregated cluster node status Required orchestrator env: PROXMOX_API_URL PROXMOX_CF_ACCESS_CLIENT_ID PROXMOX_CF_ACCESS_CLIENT_SECRET When env is missing the endpoints return 503 with an actionable JSON body and the frontend stays in its mocked state — no crashes, no partial deploys.
154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
import "dotenv/config";
|
|
import express from "express";
|
|
import cors from "cors";
|
|
import { validateEnv } from "./config/env";
|
|
import {
|
|
apiLimiter,
|
|
securityHeaders,
|
|
requestSizeLimits,
|
|
requestId,
|
|
apiKeyAuth,
|
|
auditLog,
|
|
} from "./middleware";
|
|
import { requestTimeout } from "./middleware/timeout";
|
|
import { logger } from "./logging/logger";
|
|
import { getMetrics, httpRequestDuration, httpRequestTotal, register } from "./metrics/prometheus";
|
|
import { healthCheck, readinessCheck, livenessCheck } from "./health/health";
|
|
import { listPlansEndpoint, createPlan, getPlan, addSignature, validatePlanEndpoint } from "./api/plans";
|
|
import { streamPlanStatus } from "./api/sse";
|
|
import { executionCoordinator } from "./services/execution";
|
|
import { runMigration } from "./db/migrations";
|
|
|
|
// Validate environment on startup
|
|
validateEnv();
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 8080;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(securityHeaders);
|
|
app.use(requestSizeLimits);
|
|
app.use(requestId);
|
|
app.use(requestTimeout(30000)); // 30 second timeout
|
|
app.use(express.json({ limit: "10mb" }));
|
|
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
|
|
// Request logging middleware
|
|
app.use((req, res, next) => {
|
|
const start = Date.now();
|
|
const requestId = req.headers["x-request-id"] as string || "unknown";
|
|
|
|
res.on("finish", () => {
|
|
const duration = Date.now() - start;
|
|
httpRequestDuration.observe(
|
|
{ method: req.method, route: req.route?.path || req.path, status: res.statusCode },
|
|
duration / 1000
|
|
);
|
|
httpRequestTotal.inc({ method: req.method, route: req.route?.path || req.path, status: res.statusCode });
|
|
|
|
logger.info({
|
|
req,
|
|
res,
|
|
duration,
|
|
requestId,
|
|
}, `${req.method} ${req.path} ${res.statusCode}`);
|
|
});
|
|
|
|
next();
|
|
});
|
|
|
|
// Health check endpoints (no auth required)
|
|
app.get("/health", async (req, res) => {
|
|
const health = await healthCheck();
|
|
res.status(health.status === "healthy" ? 200 : 503).json(health);
|
|
});
|
|
|
|
app.get("/ready", async (req, res) => {
|
|
const ready = await readinessCheck();
|
|
res.status(ready ? 200 : 503).json({ ready });
|
|
});
|
|
|
|
app.get("/live", async (req, res) => {
|
|
const alive = await livenessCheck();
|
|
res.status(alive ? 200 : 503).json({ alive });
|
|
});
|
|
|
|
// Metrics endpoint
|
|
app.get("/metrics", async (req, res) => {
|
|
res.setHeader("Content-Type", register.contentType);
|
|
const metrics = await getMetrics();
|
|
res.send(metrics);
|
|
});
|
|
|
|
// API routes with rate limiting
|
|
app.use("/api", apiLimiter);
|
|
|
|
// Plan management endpoints
|
|
app.get("/api/plans", listPlansEndpoint);
|
|
app.post("/api/plans", auditLog("CREATE_PLAN", "plan"), createPlan);
|
|
app.get("/api/plans/:planId", getPlan);
|
|
app.post("/api/plans/:planId/signature", addSignature);
|
|
app.post("/api/plans/:planId/validate", validatePlanEndpoint);
|
|
|
|
// Execution endpoints
|
|
import { executePlan, getExecutionStatus, abortExecution } from "./api/execution";
|
|
import { registerWebhook } from "./api/webhooks";
|
|
app.post("/api/plans/:planId/execute", auditLog("EXECUTE_PLAN", "plan"), executePlan);
|
|
app.get("/api/plans/:planId/status", getExecutionStatus);
|
|
app.post("/api/plans/:planId/abort", auditLog("ABORT_PLAN", "plan"), abortExecution);
|
|
app.post("/api/webhooks", registerWebhook);
|
|
|
|
// Proxmox BFF — forwards browser requests to the CF-Access protected
|
|
// Proxmox API using a server-side service token. See
|
|
// orchestrator/src/integrations/proxmox.ts for required env.
|
|
import { proxmoxHealth, proxmoxClusterStatus } from "./api/proxmox";
|
|
app.get("/api/proxmox/health", proxmoxHealth);
|
|
app.get("/api/proxmox/cluster/status", proxmoxClusterStatus);
|
|
|
|
app.get("/api/plans/:planId/status/stream", streamPlanStatus);
|
|
|
|
// Error handling middleware
|
|
import { errorHandler } from "./services/errorHandler";
|
|
import { initRedis } from "./services/redis";
|
|
|
|
// Initialize Redis if configured
|
|
if (process.env.REDIS_URL) {
|
|
initRedis();
|
|
}
|
|
|
|
app.use(errorHandler);
|
|
|
|
// Graceful shutdown
|
|
process.on("SIGTERM", async () => {
|
|
logger.info("SIGTERM received, shutting down gracefully");
|
|
// Close database connections
|
|
// Close SSE connections
|
|
process.exit(0);
|
|
});
|
|
|
|
process.on("SIGINT", async () => {
|
|
logger.info("SIGINT received, shutting down gracefully");
|
|
process.exit(0);
|
|
});
|
|
|
|
// Start server
|
|
async function start() {
|
|
try {
|
|
// Run database migrations
|
|
if (process.env.RUN_MIGRATIONS === "true") {
|
|
await runMigration();
|
|
}
|
|
|
|
app.listen(PORT, () => {
|
|
logger.info({ port: PORT }, "Orchestrator service started");
|
|
});
|
|
} catch (error) {
|
|
logger.error({ error }, "Failed to start server");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
start();
|
|
|