From f712479882922e4bdd481866195f2b7319634a85 Mon Sep 17 00:00:00 2001 From: Gitead Date: Sun, 12 Jul 2026 17:25:30 -0400 Subject: [PATCH] infra: serve app over HTTPS via nginx + Let's Encrypt for LAN access getDisplayMedia (screen capture) requires a secure context, so LAN staff access now goes through nginx on a real trusted cert (local-summit.mydentalofficemanagement.com, issued via certbot's Cloudflare DNS-01 plugin) instead of plain HTTP. A separate public hostname/nginx block is reserved for Twilio webhooks only, routed through Cloudflare Tunnel so nothing else is exposed to the internet. Also fixes the backend CORS allowlist for the new hostname, and stops the Socket.IO client from bypassing nginx to hit the backend directly on :5000 (which broke under TLS/mixed content). --- apps/Backend/.env | 2 +- apps/Frontend/.gitignore | 3 ++ apps/Frontend/src/lib/socket.ts | 14 +++++---- apps/Frontend/vite.config.ts | 1 + nginx.conf | 52 +++++++++++++++++++++++++++++++-- 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/apps/Backend/.env b/apps/Backend/.env index 637f1fed..b26a93d4 100755 --- a/apps/Backend/.env +++ b/apps/Backend/.env @@ -2,7 +2,7 @@ NODE_ENV="development" HOST=0.0.0.0 PORT=5000 CLOUDFLARE_HOST= -FRONTEND_URLS=http://localhost:3000 +FRONTEND_URLS=http://localhost:3000,https://local-summit.mydentalofficemanagement.com SELENIUM_AGENT_BASE_URL=http://localhost:5002 JWT_SECRET = 'dentalsecret' LICENSE_SECRET=3aa4ab937e46c6863b9e3c2b591a595b31ea3af1060bf5e7961ad722a8b54f92 diff --git a/apps/Frontend/.gitignore b/apps/Frontend/.gitignore index a547bf36..07975b4c 100755 --- a/apps/Frontend/.gitignore +++ b/apps/Frontend/.gitignore @@ -12,6 +12,9 @@ dist dist-ssr *.local +# Local HTTPS dev certs (machine-specific, generated via mkcert) +certs/ + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/apps/Frontend/src/lib/socket.ts b/apps/Frontend/src/lib/socket.ts index 964ceb96..18e03758 100644 --- a/apps/Frontend/src/lib/socket.ts +++ b/apps/Frontend/src/lib/socket.ts @@ -6,13 +6,17 @@ */ import { io, Socket } from "socket.io-client"; -// Connect directly to backend to avoid Vite's WS proxy failing on upgrade, -// which causes an unhandled AggregateError from engine.io's Promise.any() probe. -// Use the env var when set; otherwise derive the backend URL from the current -// page's hostname so remote browsers (non-localhost) reach the server correctly. +// When served through nginx (any port other than Vite's own 3000), nginx already +// proxies /socket.io/ with WS upgrade support — connect same-origin so TLS and +// CORS are handled consistently with the rest of the app. +// When hitting Vite's dev server directly on :3000, connect straight to the +// backend instead, since Vite's own WS proxy fails on upgrade (unhandled +// AggregateError from engine.io's Promise.any() probe). const SOCKET_URL = import.meta.env.VITE_API_BASE_URL_BACKEND || - `${window.location.protocol}//${window.location.hostname}:5000`; + (window.location.port === "3000" + ? `${window.location.protocol}//${window.location.hostname}:5000` + : window.location.origin); export const socket: Socket = io(SOCKET_URL, { withCredentials: true, diff --git a/apps/Frontend/vite.config.ts b/apps/Frontend/vite.config.ts index 74ac4ca8..9ae6d238 100755 --- a/apps/Frontend/vite.config.ts +++ b/apps/Frontend/vite.config.ts @@ -16,6 +16,7 @@ export default defineConfig(({ mode }) => { allowedHosts: [ ...(env.VITE_CLOUDFLARE_HOST ? [env.VITE_CLOUDFLARE_HOST] : []), "192.168.0.94", + "local-summit.mydentalofficemanagement.com", ], proxy: { "/api": { diff --git a/nginx.conf b/nginx.conf index 83d8a305..108da622 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,6 +1,17 @@ +# ── LAN-only app access (staff) ────────────────────────────────────────── +# https://local-summit.mydentalofficemanagement.com +# DNS A record points at this office's private LAN IP; cert is a real +# Let's Encrypt cert (issued via certbot + Cloudflare DNS-01), so no CA +# needs to be installed on any staff PC. Restricted to the office subnet. server { - listen 80; - server_name _; + listen 443 ssl; + server_name local-summit.mydentalofficemanagement.com; + + ssl_certificate /etc/letsencrypt/live/local-summit.mydentalofficemanagement.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/local-summit.mydentalofficemanagement.com/privkey.pem; + + allow 192.168.0.0/24; + deny all; client_max_body_size 50m; @@ -33,3 +44,40 @@ server { proxy_set_header Host $host; } } + +# ── Public Twilio webhooks only ────────────────────────────────────────── +# https://summit.mydentalofficemanagement.com, reached via Cloudflare Tunnel. +# Cloudflare terminates TLS at its edge and cloudflared forwards plain HTTP +# to this block, so no certificate is needed here. Nothing except the +# Twilio webhook path is exposed on this hostname. +server { + listen 80; + server_name summit.mydentalofficemanagement.com; + + client_max_body_size 50m; + + location /api/twilio/ { + proxy_pass http://localhost:5000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location / { + return 403; + } +} + +# Catch-all: reject anything not matching the two hostnames above +# (e.g. plain http:// or an unrecognized Host header). +server { + listen 80 default_server; + listen 443 ssl default_server; + server_name _; + + ssl_certificate /etc/letsencrypt/live/local-summit.mydentalofficemanagement.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/local-summit.mydentalofficemanagement.com/privkey.pem; + + return 403; +}