diff --git a/server/apiServer.ts b/server/apiServer.ts new file mode 100644 index 00000000..424df49d --- /dev/null +++ b/server/apiServer.ts @@ -0,0 +1,56 @@ +import express, { Request, Response } from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import config from "@server/config"; +import logger from "@server/logger"; +import { errorHandlerMiddleware, notFoundMiddleware, rateLimitMiddleware } from "@server/middlewares"; +import { authenticated, unauthenticated } from "@server/routers/external"; +import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; +import { logIncomingMiddleware } from "./middlewares/logIncoming"; + +const dev = process.env.ENVIRONMENT !== "prod"; +const externalPort = config.server.external_port; + +export function createApiServer() { + const apiServer = express(); + + // Middleware setup + apiServer.set("trust proxy", 1); + apiServer.use(cors()); + apiServer.use(cookieParser()); + apiServer.use(express.json()); + + if (!dev) { + apiServer.use( + rateLimitMiddleware({ + windowMin: config.rate_limit.window_minutes, + max: config.rate_limit.max_requests, + type: "IP_ONLY", + }) + ); + } + + // API routes + const prefix = `/api/v1`; + apiServer.use(logIncomingMiddleware); + apiServer.use(prefix, unauthenticated); + apiServer.use(prefix, authenticated); + + // WebSocket routes + apiServer.use(`/ws`, wsRouter); + + // Error handling + apiServer.use(notFoundMiddleware); + apiServer.use(errorHandlerMiddleware); + + // Create HTTP server + const httpServer = apiServer.listen(externalPort, (err?: any) => { + if (err) throw err; + logger.info(`API server is running on http://localhost:${externalPort}`); + }); + + // Handle WebSocket upgrades + handleWSUpgrade(httpServer); + + return httpServer; +} diff --git a/server/config.ts b/server/config.ts index ee1cc3ea..b64a4027 100644 --- a/server/config.ts +++ b/server/config.ts @@ -21,6 +21,7 @@ const environmentSchema = z.object({ server: z.object({ external_port: portSchema, internal_port: portSchema, + next_port: portSchema, internal_hostname: z.string(), secure_cookies: z.boolean(), signup_secret: z.string().optional(), diff --git a/server/index.ts b/server/index.ts index f1c5e79b..56e985f7 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,107 +1,35 @@ -import config from "@server/config"; -import express, { Request, Response } from "express"; -import next from "next"; -import { parse } from "url"; -import logger from "@server/logger"; -import helmet from "helmet"; -import cors from "cors"; -import { - errorHandlerMiddleware, - notFoundMiddleware, - rateLimitMiddleware, -} from "@server/middlewares"; -import internal from "@server/routers/internal"; -import { authenticated, unauthenticated } from "@server/routers/external"; -import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; -import cookieParser from "cookie-parser"; -import { User, UserOrg } from "@server/db/schema"; import { ensureActions } from "./db/ensureActions"; -import { logIncomingMiddleware } from "./middlewares/logIncoming"; +import { createApiServer } from "./apiServer"; +import { createNextServer } from "./nextServer"; +import { createInternalServer } from "./internalServer"; +import { User, UserOrg } from "./db/schema"; -const dev = process.env.ENVIRONMENT !== "prod"; +async function startServers() { + await ensureActions(); + + // Start all servers + const apiServer = createApiServer(); + const nextServer = await createNextServer(); + const internalServer = createInternalServer(); -const app = next({ dev }); -const handle = app.getRequestHandler(); - -const externalPort = config.server.external_port; -const internalPort = config.server.internal_port; - -app.prepare().then(() => { - ensureActions(); // This loads the actions into the database - - // External server - const externalServer = express(); - externalServer.set("trust proxy", 1); - - // externalServer.use(helmet()); // Disabled because causes issues with Next.js - externalServer.use(cors()); - externalServer.use(cookieParser()); - externalServer.use(express.json()); - if (!dev) { - externalServer.use( - rateLimitMiddleware({ - windowMin: config.rate_limit.window_minutes, - max: config.rate_limit.max_requests, - type: "IP_ONLY", - }) - ); - } - - const prefix = `/api/v1`; - externalServer.use(logIncomingMiddleware); - externalServer.use(prefix, unauthenticated); - externalServer.use(prefix, authenticated); - // externalServer.use(`${prefix}/ws`, wsRouter); - - externalServer.use(notFoundMiddleware); - - // We are using NEXT from here on - externalServer.all("*", (req: Request, res: Response) => { - const parsedUrl = parse(req.url!, true); - handle(req, res, parsedUrl); - }); - - const httpServer = externalServer.listen(externalPort, (err?: any) => { - if (err) throw err; - logger.info( - `Main server is running on http://localhost:${externalPort}` - ); - }); - - // handleWSUpgrade(httpServer); - - externalServer.use(errorHandlerMiddleware); - - // Internal server - const internalServer = express(); - - internalServer.use(helmet()); - internalServer.use(cors()); - internalServer.use(cookieParser()); - internalServer.use(express.json()); - - internalServer.use(prefix, internal); - - internalServer.listen(internalPort, (err?: any) => { - if (err) throw err; - logger.info( - `Internal server is running on http://localhost:${internalPort}` - ); - }); - - internalServer.use(notFoundMiddleware); - internalServer.use(errorHandlerMiddleware); -}); - -declare global { - // TODO: eventually make seperate types that extend express.Request - namespace Express { - interface Request { - user?: User; - userOrg?: UserOrg; - userOrgRoleId?: number; - userOrgId?: string; - userOrgIds?: string[]; - } - } + return { + apiServer, + nextServer, + internalServer + }; } + +// Types +declare global { + namespace Express { + interface Request { + user?: User; + userOrg?: UserOrg; + userOrgRoleId?: number; + userOrgId?: string; + userOrgIds?: string[]; + } + } +} + +startServers().catch(console.error); diff --git a/server/internalServer.ts b/server/internalServer.ts new file mode 100644 index 00000000..b846d8f4 --- /dev/null +++ b/server/internalServer.ts @@ -0,0 +1,32 @@ +import express from "express"; +import helmet from "helmet"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import config from "@server/config"; +import logger from "@server/logger"; +import { errorHandlerMiddleware, notFoundMiddleware } from "@server/middlewares"; +import internal from "@server/routers/internal"; + +const internalPort = config.server.internal_port; + +export function createInternalServer() { + const internalServer = express(); + + internalServer.use(helmet()); + internalServer.use(cors()); + internalServer.use(cookieParser()); + internalServer.use(express.json()); + + const prefix = `/api/v1`; + internalServer.use(prefix, internal); + + internalServer.use(notFoundMiddleware); + internalServer.use(errorHandlerMiddleware); + + internalServer.listen(internalPort, (err?: any) => { + if (err) throw err; + logger.info(`Internal server is running on http://localhost:${internalPort}`); + }); + + return internalServer; +} \ No newline at end of file diff --git a/server/nextServer.ts b/server/nextServer.ts new file mode 100644 index 00000000..d858463e --- /dev/null +++ b/server/nextServer.ts @@ -0,0 +1,29 @@ +import next from "next"; +import express from "express"; +import { parse } from "url"; +import logger from "@server/logger"; +import config from "@server/config"; + +const nextPort = config.server.next_port; + +export async function createNextServer() { +// const app = next({ dev }); + const app = next({ dev: process.env.ENVIRONMENT !== "prod" }); + const handle = app.getRequestHandler(); + + await app.prepare(); + + const nextServer = express(); + + nextServer.all("*", (req, res) => { + const parsedUrl = parse(req.url!, true); + return handle(req, res, parsedUrl); + }); + + nextServer.listen(nextPort, (err?: any) => { + if (err) throw err; + logger.info(`Next.js server is running on http://localhost:${nextPort}`); + }); + + return nextServer; +} \ No newline at end of file diff --git a/server/routers/ws.ts b/server/routers/ws.ts index a5076c4c..95947e24 100644 --- a/server/routers/ws.ts +++ b/server/routers/ws.ts @@ -17,7 +17,6 @@ interface WebSocketRequest extends IncomingMessage { interface AuthenticatedWebSocket extends WebSocket { newt?: Newt; - isAlive?: boolean; } interface TokenPayload { @@ -124,77 +123,23 @@ const verifyToken = async (token: string): Promise => { return { newt: existingNewt[0], session }; } catch (error) { - console.error("Token verification failed:", error); + logger.error("Token verification failed:", error); return null; } }; -// Router endpoint (unchanged) -router.get("/ws", (req: Request, res: Response) => { - res.status(200).send("WebSocket endpoint"); -}); +const setupConnection = (ws: AuthenticatedWebSocket, newt: Newt): void => { + logger.info("Establishing websocket connection"); -// WebSocket upgrade handler -const handleWSUpgrade = (server: HttpServer): void => { - server.on("upgrade", async (request: WebSocketRequest, socket: Socket, head: Buffer) => { - try { - const token = request.url?.includes("?") - ? new URLSearchParams(request.url.split("?")[1]).get("token") || "" - : request.headers["sec-websocket-protocol"]; - - if (!token) { - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - - const tokenPayload = await verifyToken(token); - if (!tokenPayload) { - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); - socket.destroy(); - return; - } - - request.token = token; - - wss.handleUpgrade(request, socket, head, (ws: AuthenticatedWebSocket) => { - ws.newt = tokenPayload.newt; - ws.isAlive = true; - wss.emit("connection", ws, request); - }); - } catch (error) { - console.error("Upgrade error:", error); - socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n"); - socket.destroy(); - } - }); -}; - -// WebSocket connection handler -wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) => { - const newtId = ws.newt?.newtId; - if (!newtId) { - console.error("Connection attempt without newt ID"); + if (!newt) { + logger.error("Connection attempt without newt"); return ws.terminate(); } + ws.newt = newt; + // Add client to tracking - addClient(newtId, ws); - - // Set up ping-pong for connection health check - const pingInterval = setInterval(() => { - if (ws.isAlive === false) { - clearInterval(pingInterval); - removeClient(newtId, ws); - return ws.terminate(); - } - ws.isAlive = false; - ws.ping(); - }, 30000); - - ws.on("pong", () => { - ws.isAlive = true; - }); + addClient(newt.newtId, ws); ws.on("message", async (data) => { try { @@ -226,7 +171,7 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) => if (response) { if (response.broadcast) { // Broadcast to all clients except sender if specified - broadcastToAllExcept(response.message, response.excludeSender ? newtId : undefined); + broadcastToAllExcept(response.message, response.excludeSender ? newt.newtId : undefined); } else if (response.targetNewtId) { // Send to specific client if targetNewtId is provided sendToClient(response.targetNewtId, response.message); @@ -235,9 +180,9 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) => ws.send(JSON.stringify(response.message)); } } - + } catch (error) { - console.error("Message handling error:", error); + logger.error("Message handling error:", error); ws.send(JSON.stringify({ type: "error", data: { @@ -247,18 +192,58 @@ wss.on("connection", (ws: AuthenticatedWebSocket, request: WebSocketRequest) => })); } }); - + ws.on("close", () => { - clearInterval(pingInterval); - removeClient(newtId, ws); - logger.info(`Client disconnected - Newt ID: ${newtId}`); + removeClient(newt.newtId, ws); + logger.info(`Client disconnected - Newt ID: ${newt.newtId}`); + }); + + ws.on("error", (error: Error) => { + logger.error(`WebSocket error for Newt ID ${newt.newtId}:`, error); }); - ws.on("error", (error: Error) => { - console.error(`WebSocket error for Newt ID ${newtId}:`, error); - }); + logger.info(`WebSocket connection established - Newt ID: ${newt.newtId}`); +}; + +// Router endpoint (unchanged) +router.get("/ws", (req: Request, res: Response) => { + res.status(200).send("WebSocket endpoint"); }); +// WebSocket upgrade handler +const handleWSUpgrade = (server: HttpServer): void => { + server.on("upgrade", async (request: WebSocketRequest, socket: Socket, head: Buffer) => { + try { + const token = request.url?.includes("?") + ? new URLSearchParams(request.url.split("?")[1]).get("token") || "" + : request.headers["sec-websocket-protocol"]; + + if (!token) { + logger.warn("Unauthorized connection attempt: no token..."); + socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + socket.destroy(); + return; + } + + const tokenPayload = await verifyToken(token); + if (!tokenPayload) { + logger.warn("Unauthorized connection attempt: invalid token..."); + socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + socket.destroy(); + return; + } + + wss.handleUpgrade(request, socket, head, (ws: AuthenticatedWebSocket) => { + setupConnection(ws, tokenPayload.newt); + }); + } catch (error) { + logger.error("WebSocket upgrade error:", error); + socket.write("HTTP/1.1 500 Internal Server Error\r\n\r\n"); + socket.destroy(); + } + }); +}; + export { router, handleWSUpgrade, diff --git a/src/app/[orgId]/settings/sites/[niceId]/components/NewtConfig.tsx b/src/app/[orgId]/settings/sites/[niceId]/components/NewtConfig.tsx deleted file mode 100644 index e3e40dd8..00000000 --- a/src/app/[orgId]/settings/sites/[niceId]/components/NewtConfig.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -export function NewtConfig() { - const config = `curl -fsSL https://get.docker.com -o get-docker.sh -sh get-docker.sh`; - - return ( -
-            {config}
-        
- ); -} diff --git a/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx index 85e81b43..2460583a 100644 --- a/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx +++ b/src/app/[orgId]/settings/sites/components/CreateSiteForm.tsx @@ -174,7 +174,13 @@ Endpoint = ${siteDefaults.endpoint}:${siteDefaults.listenPort} PersistentKeepalive = 5` : ""; - const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret}`; + // am I at http or https? + let proto = "http:"; + if (typeof window !== "undefined") { + proto = window.location.protocol; + } + + const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`; return ( <>