mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:17:21 +00:00
- Add root '/' endpoint that responds with JSON status - Make download routes handle missing installers gracefully - Add error handling around download routes registration - Add logging for download routes initialization Fixes Railway healthcheck failures by ensuring server responds at root path
196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
import dotenv from "dotenv";
|
|
|
|
// Load environment variables
|
|
dotenv.config({ path: './.env' });
|
|
|
|
import express, { type Request, Response, NextFunction } from "express";
|
|
import session from "express-session";
|
|
import cors from "cors";
|
|
import { registerRoutes } from "./routes.js";
|
|
import { serveStatic } from "./static.js";
|
|
import { createServer } from "http";
|
|
import { setupWebSocket, websocket } from "./websocket.js";
|
|
import { attachOrgContext, requireOrgMember } from "./org-middleware.js";
|
|
import { getCorsOptions } from "./cors-config.js";
|
|
import downloadRoutes from "./download-routes.js";
|
|
|
|
const app = express();
|
|
const httpServer = createServer(app);
|
|
|
|
// CORS configuration for all AeThex domains
|
|
app.use(cors(getCorsOptions()));
|
|
|
|
// Health check for Railway/monitoring
|
|
app.get("/health", (_req, res) => {
|
|
res.json({ status: "healthy", timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// Root health check (Railway uses this)
|
|
app.get("/", (_req, res) => {
|
|
res.json({
|
|
status: "online",
|
|
service: "AeThex OS API",
|
|
version: "1.0.0",
|
|
timestamp: new Date().toISOString(),
|
|
endpoints: {
|
|
web: "Use client to access web interface",
|
|
api: "/api/*",
|
|
download: "/api/download/*"
|
|
}
|
|
});
|
|
});
|
|
|
|
// API status endpoint (moved from root to /api/status)
|
|
app.get("/api/status", (_req, res) => {
|
|
const isKernel = process.env.OPS_Version ? true : false;
|
|
|
|
res.json({
|
|
status: isKernel ? "AeThex OS Unikernel: ONLINE" : "AeThex OS Simulation: ONLINE",
|
|
mode: isKernel ? "KERNEL_NATIVE" : "HOSTED_SIMULATION",
|
|
version: "1.0.0",
|
|
endpoints: {
|
|
link: "/api/os/link/*",
|
|
entitlements: "/api/os/entitlements/*",
|
|
subjects: "/api/os/subjects/*"
|
|
}
|
|
});
|
|
});
|
|
|
|
// Trust proxy for proper cookie handling behind Vite dev server
|
|
app.set("trust proxy", 1);
|
|
|
|
declare module "http" {
|
|
interface IncomingMessage {
|
|
rawBody: unknown;
|
|
}
|
|
}
|
|
|
|
// Require session secret in production
|
|
const sessionSecret = process.env.SESSION_SECRET;
|
|
if (process.env.NODE_ENV === "production" && !sessionSecret) {
|
|
throw new Error("SESSION_SECRET environment variable is required in production");
|
|
}
|
|
|
|
// Session configuration with security best practices
|
|
const isProduction = process.env.NODE_ENV === "production";
|
|
|
|
// For Unikernel local testing (localhost:8080), we must disable secure cookies
|
|
// because there is no SSL termination in the kernel itself yet.
|
|
// We detect if we are running in OPS/Nanos via the environment, or just assume
|
|
// that if we are "production" but not explicitly requiring HTTPS, we might be in a kernel.
|
|
// A safer check: if we are in OPS, process.env.OPS_Version might be present (if set),
|
|
// OR we can just relax this for now to fix the login loop.
|
|
const isSecure = isProduction && process.env.REQUIRE_HTTPS === "true";
|
|
|
|
app.use(
|
|
session({
|
|
secret: sessionSecret || "dev-only-secret-not-for-prod",
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
secure: isSecure,
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
},
|
|
proxy: true, // Always trust proxy (Replit uses reverse proxy in both dev and prod)
|
|
})
|
|
);
|
|
|
|
|
|
app.use(
|
|
express.json({
|
|
verify: (req, _res, buf) => {
|
|
req.rawBody = buf;
|
|
},
|
|
}),
|
|
);
|
|
|
|
app.use(express.urlencoded({ extended: false }));
|
|
|
|
export function log(message: string, source = "express") {
|
|
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
hour12: true,
|
|
});
|
|
|
|
console.log(`${formattedTime} [${source}] ${message}`);
|
|
}
|
|
|
|
app.use((req, res, next) => {
|
|
const start = Date.now();
|
|
const path = req.path;
|
|
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
|
|
|
const originalResJson = res.json;
|
|
res.json = function (bodyJson, ...args) {
|
|
capturedJsonResponse = bodyJson;
|
|
return originalResJson.apply(res, [bodyJson, ...args]);
|
|
};
|
|
|
|
res.on("finish", () => {
|
|
const duration = Date.now() - start;
|
|
if (path.startsWith("/api")) {
|
|
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
|
if (capturedJsonResponse) {
|
|
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
|
}
|
|
|
|
log(logLine);
|
|
}
|
|
});
|
|
|
|
next();
|
|
});
|
|
|
|
|
|
(async () => {
|
|
// Register download routes (wrapped in try-catch for safety)
|
|
try {
|
|
app.use('/api/download', downloadRoutes);
|
|
log("Download routes registered", "express");
|
|
} catch (error) {
|
|
log(`Warning: Failed to register download routes: ${error}`, "express");
|
|
// Continue anyway - app can work without download routes
|
|
}
|
|
|
|
// Register routes (org middleware applied selectively within routes.ts)
|
|
await registerRoutes(httpServer, app);
|
|
|
|
// Setup WebSocket server for real-time notifications and Aegis alerts
|
|
const io = setupWebSocket(httpServer);
|
|
websocket.setIO(io);
|
|
log("WebSocket server initialized", "websocket");
|
|
|
|
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
|
const status = err.status || err.statusCode || 500;
|
|
const message = err.message || "Internal Server Error";
|
|
res.status(status).json({ message });
|
|
throw err;
|
|
});
|
|
|
|
// importantly only setup vite in development and after
|
|
// setting up all the other routes so the catch-all route
|
|
// doesn't interfere with the other routes
|
|
if (process.env.NODE_ENV === "production") {
|
|
serveStatic(app);
|
|
} else {
|
|
const { setupVite } = await import("./vite");
|
|
await setupVite(httpServer, app);
|
|
}
|
|
|
|
// ALWAYS serve the app on the port specified in the environment variable PORT
|
|
// Other ports are firewalled. Default to 5000 if not specified.
|
|
// this serves both the API and the client.
|
|
// It is the only port that is not firewalled.
|
|
const port = parseInt(process.env.PORT || "5000", 10);
|
|
httpServer.listen(
|
|
port,
|
|
() => {
|
|
log(`serving on port ${port}`);
|
|
log(`WebSocket available at ws://localhost:${port}/socket.io`, "websocket");
|
|
},
|
|
);
|
|
})();
|