From 42f9a80e51971ec4c90809d59311e4c4d1538c01 Mon Sep 17 00:00:00 2001 From: Lucas Vieira Date: Thu, 12 Sep 2024 21:25:24 -0300 Subject: [PATCH] chore: add an nginx to compose so that user only needs to expose a single port --- apps/api/src/websocket/index.ts | 31 +++++++++++++ apps/api/src/yjs/v2/index.ts | 3 +- apps/web/src/hooks/useWebsocket.tsx | 6 ++- apps/web/src/utils/env.ts | 16 +++++-- docker-compose.yaml | 30 ++++++------ nginx/nginx.conf | 71 +++++++++++++++++++++++++++++ 6 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 nginx/nginx.conf diff --git a/apps/api/src/websocket/index.ts b/apps/api/src/websocket/index.ts index d9d819c..51bcbdc 100644 --- a/apps/api/src/websocket/index.ts +++ b/apps/api/src/websocket/index.ts @@ -53,6 +53,7 @@ type Server = { export function createSocketServer(server: http.Server): Server { const io: IOServer = new BaseServer(server, { cors: { credentials: true, origin: config().FRONTEND_URL }, + transports: ['websocket'], }) io.use(async (socket: Socket, next) => { @@ -68,6 +69,13 @@ export function createSocketServer(server: http.Server): Server { next(new Error('Unauthorized')) } } catch (err) { + logger.error( + { + err, + socketId: socket.id, + }, + 'Error authenticating socket connection' + ) next(new Error('Internal Server Error')) } }) @@ -75,8 +83,17 @@ export function createSocketServer(server: http.Server): Server { let workInProgress: Map> = new Map() io.on('connection', (socket: Socket) => { + logger.info({ socketId: socket.id }, 'Client connected to socket server') + const session = socket.session if (!session) { + logger.error( + { + socketId: socket.id, + }, + 'Socket connection did not have a session' + ) + socket.disconnect(true) return } @@ -104,6 +121,20 @@ export function createSocketServer(server: http.Server): Server { trackWork(handleRestartEnvironment(socket, session)) ) socket.on('complete-python', trackWork(completePython(io, socket, session))) + + socket.on('disconnect', (reason) => { + logger.info( + { socketId: socket.id, reason }, + 'Client disconnected from socket server' + ) + }) + + socket.on('error', (error) => { + logger.error( + { socketId: socket.id, error }, + 'Socket server error occurred' + ) + }) }) return { diff --git a/apps/api/src/yjs/v2/index.ts b/apps/api/src/yjs/v2/index.ts index b090c66..a210f8a 100644 --- a/apps/api/src/yjs/v2/index.ts +++ b/apps/api/src/yjs/v2/index.ts @@ -68,8 +68,7 @@ async function getRequestData(req: http.IncomingMessage): Promise<{ } | null> { const cookiesHeader = req.headers.cookie const cookies = cookie.parse(cookiesHeader ?? '') - const url = new URL(req.url ?? '', config().API_URL) - const query = qs.parse(url.search.slice(1)) + const query = qs.parse(req.url?.split('?')[1] ?? '') const docId = query['documentId'] const clock = parseInt((query['clock'] ?? '').toString()) const isDataApp = query['isDataApp'] === 'true' diff --git a/apps/web/src/hooks/useWebsocket.tsx b/apps/web/src/hooks/useWebsocket.tsx index b1f9645..24c6d76 100644 --- a/apps/web/src/hooks/useWebsocket.tsx +++ b/apps/web/src/hooks/useWebsocket.tsx @@ -16,8 +16,12 @@ export function WebsocketProvider({ children }: Props) { const workspaceId = useStringQuery('workspaceId') useEffect(() => { if (session.data) { - const socket = io(NEXT_PUBLIC_API_URL(), { + const url = new URL(NEXT_PUBLIC_API_URL()) + const withoutPathname = url.origin + const socket = io(withoutPathname, { withCredentials: true, + path: url.pathname + '/socket.io', + transports: ['websocket'], }) setSocket(socket) diff --git a/apps/web/src/utils/env.ts b/apps/web/src/utils/env.ts index 2704b11..b47fe19 100644 --- a/apps/web/src/utils/env.ts +++ b/apps/web/src/utils/env.ts @@ -11,14 +11,24 @@ function getFromWindow(key: string, or?: string): string { } } +function currentUrl() { + return `${window.location.protocol}//${window.location.host}` +} + export const NEXT_PUBLIC_API_URL = () => - process.env.NEXT_PUBLIC_API_URL || getFromWindow('NEXT_PUBLIC_API_URL') + process.env.NEXT_PUBLIC_API_URL || + getFromWindow('NEXT_PUBLIC_API_URL') || + `${currentUrl()}/api` export const NEXT_PUBLIC_API_WS_URL = () => - process.env.NEXT_PUBLIC_API_WS_URL || getFromWindow('NEXT_PUBLIC_API_WS_URL') + process.env.NEXT_PUBLIC_API_WS_URL || + getFromWindow('NEXT_PUBLIC_API_WS_URL') || + `${currentUrl().replace('http', 'ws')}/api` export const NEXT_PUBLIC_PUBLIC_URL = () => - process.env.NEXT_PUBLIC_PUBLIC_URL || getFromWindow('NEXT_PUBLIC_PUBLIC_URL') + process.env.NEXT_PUBLIC_PUBLIC_URL || + getFromWindow('NEXT_PUBLIC_PUBLIC_URL') || + currentUrl() export const NEXT_PUBLIC_GATEWAY_IP = () => process.env.NEXT_PUBLIC_GATEWAY_IP || diff --git a/docker-compose.yaml b/docker-compose.yaml index 5afdbec..c621fca 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -76,18 +76,8 @@ services: build: context: '.' dockerfile: 'apps/web/Dockerfile' - args: - NODE_ENV: 'production' - NEXT_PUBLIC_API_URL: 'https://api.${TLD:?error}' - NEXT_PUBLIC_API_WS_URL: 'wss://api.${TLD:?error}' - NEXT_PUBLIC_PUBLIC_URL: 'https://app.${TLD:?error}' environment: NODE_ENV: 'production' - NEXT_PUBLIC_API_URL: 'https://api.${TLD:?error}' - NEXT_PUBLIC_API_WS_URL: 'wss://api.${TLD:?error}' - NEXT_PUBLIC_PUBLIC_URL: 'https://app.${TLD:?error}' - ports: - - '3000:3000' depends_on: api: condition: service_healthy @@ -97,14 +87,11 @@ services: build: context: '.' dockerfile: 'apps/api/Dockerfile' - ports: - - '8080:8080' environment: NODE_ENV: 'production' - LOG_LEVEL: 'info' - API_URL: 'https://api.${TLD:?error}' - FRONTEND_URL: 'https://app.${TLD:?error}' - TLD: ${TLD:?error} + LOG_LEVEL: 'debug' + API_URL: '/api' + FRONTEND_URL: '/' LOGIN_JWT_SECRET: ${LOGIN_JWT_SECRET:?error} AUTH_JWT_SECRET: ${AUTH_JWT_SECRET:?error} AI_API_URL: 'http://ai:8000' @@ -138,6 +125,17 @@ services: ai: condition: service_healthy + nginx: + image: nginx:latest + depends_on: + - web + - api + ports: + - '3000:3000' + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + restart: always + volumes: jupyter: postgres_data: diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..490bdfc --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,71 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + + upstream web { + server web:3000; + } + + upstream api { + server api:8080; + } + + server { + listen 3000; + + # Handle WebSocket connections for /api/socket.io + location /api/socket.io/ { + proxy_pass http://api/socket.io/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60m; + proxy_send_timeout 60m; + proxy_buffering off; # Disable buffering for WebSocket + } + + location /api/v2/yjs/ { + proxy_pass http://api/v2/yjs/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60m; + proxy_send_timeout 60m; + proxy_buffering off; # Disable buffering for WebSocket + } + + # Proxy regular HTTP requests to /api (non-WebSocket) to the API service + location /api/ { + proxy_pass http://api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy everything else to the frontend service + location / { + proxy_pass http://web/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +}