diff --git a/.dockerignore b/.dockerignore index 69d2603..1fb9d5a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ # Ignore local environment files **/*.env +!.env .env.local # Ignore build output directories diff --git a/.gitignore b/.gitignore index 2fca1e1..ac87341 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ .elasticbeanstalk/* !.elasticbeanstalk/*.cfg.yml !.elasticbeanstalk/*.global.yml -.env \ No newline at end of file +.env + +*.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 93b155a..c9be175 100644 --- a/README.md +++ b/README.md @@ -174,13 +174,183 @@ npm run build # Check for TypeScript errors ## Deployment -### Backend +### AWS Elastic Beanstalk (Recommended for Production) + +This application is configured for deployment on AWS Elastic Beanstalk with Docker Compose. + +#### Prerequisites + +- AWS Account +- EB CLI installed (`pip install awsebcli`) +- Docker and Docker Compose configured + +#### Deployment Steps + +1. **Initialize Elastic Beanstalk** +```bash +eb init -p docker waffle-chat --region ap-southeast-2 +``` + +2. **Create Environment with Load Balancer** +```bash +eb create waffle-chat-env --instance-type t2.small --elb-type application +``` + +⚠️ **CRITICAL**: Always create with load balancer and enable sticky sessions from the start! + +3. **Configure Environment Variables** +```bash +eb setenv \ + PORT=5000 \ + MONGO_URI="your-mongodb-uri" \ + NODE_ENV=production \ + JWT_SECRET="your-secret" \ + CLIENT_URL="http://your-domain.com" \ + CLOUDINARY_CLOUD_NAME="your-cloud-name" \ + CLOUDINARY_API_KEY="your-api-key" \ + CLOUDINARY_API_SECRET="your-api-secret" +``` + +4. **Enable Sticky Sessions** (REQUIRED for Socket.IO) + - Go to AWS Console → Elastic Beanstalk → Configuration → Load Balancer + - Edit default process + - Enable **Stickiness** → `Load balancer generated cookie` + - Set **Cookie duration**: `86400` (1 day) + - Save and apply + +5. **Deploy Application** +```bash +# Create deployment package +zip -r deploy.zip . -x "*.git*" "*node_modules/*" "*.env" "*.zip" + +# Deploy +eb deploy +``` + +6. **Configure HTTPS** (Required for microphone access) + - Request SSL certificate in AWS Certificate Manager + - Add HTTPS listener on port 443 in Load Balancer configuration + - Enable HTTP → HTTPS redirect + +#### Common Deployment Issues & Solutions + +##### Issue 1: CSP Blocking Resources +**Symptoms**: Image previews not showing, inline styles blocked, WebSocket connections failing + +**Solution**: Update `nginx.conf` to include proper CSP directives: +```nginx +# Add blob: to img-src for image previews +set $CSP_image "img-src 'self' data: blob: https://res.cloudinary.com"; + +# Add media-src for audio/video +set $CSP_media "media-src 'self' blob: data: https://res.cloudinary.com"; + +# Include in final CSP +set $CSP "default-src 'self'; ${CSP_script}; ${CSP_style}; ${CSP_image}; ${CSP_media}; ${CSP_connect}; ${CSP_font}; ${CSP_frame}; ${CSP_object}; ${CSP_base}"; +``` + +##### Issue 2: Socket.IO Connection Fails with Load Balancer +**Symptoms**: 500 errors, WebSocket upgrade fails, real-time features not working + +**Root Cause**: Without sticky sessions, each request hits a different instance. Socket.IO's multi-step handshake breaks because: +- Step 1 (polling): Goes to Instance A → Creates session +- Step 2 (WebSocket upgrade): Goes to Instance B → Doesn't know about session → FAILS + +**Solution**: ALWAYS enable sticky sessions (see step 4 above) + +**Why it works**: Sticky sessions use cookies (AWSALB) to route all requests from same user to same instance, maintaining Socket.IO connection state. + +##### Issue 3: Deployment Timeout +**Symptoms**: `Command execution completed on all instances. Summary: [Successful: 0, TimedOut: 1]` + +**Solutions**: +1. Increase timeout: `eb deploy --timeout 20` +2. Terminate stuck instance via EC2 Console (load balancer auto-creates new one) +3. Restart environment: `eb restart` +4. If persistent, create new environment with clean state + +##### Issue 4: Corrupted Dockerfile in Deployment +**Symptoms**: `ERROR: failed to solve: dockerfile parse error: unknown instruction: pax_global_header` + +**Cause**: Archive created with tar before zip (double-archiving) + +**Solution**: Create ZIP directly without tar: +```bash +zip -r deploy.zip . -x "*.git*" "*node_modules/*" "*.env" +``` + +Never do: `tar -czf project.tar.gz . && zip deploy.zip project.tar.gz` + +##### Issue 5: SSM Agent Not Online / Can't SSH +**Symptoms**: Session Manager shows "SSM Agent is not online" + +**Solutions**: +1. Use AWS Console → EC2 → Instance Connect (alternative to SSM) +2. Add EC2 key pair to environment: + - Configuration → Security → Edit → Select EC2 key pair + - Apply (will recreate instance) +3. For logs without SSH: `eb logs --all` + +##### Issue 6: Missing Environment Variable Warnings +**Symptoms**: `The "CLIENT_URL" variable is not set. Defaulting to a blank string` + +**Solution**: Set via EB CLI or Console: +```bash +eb setenv CLIENT_URL=https://your-domain.com +``` + +##### Issue 7: Microphone Access Denied +**Symptoms**: Browser blocks microphone access with "Not allowed" error + +**Cause**: Modern browsers require HTTPS for microphone/camera access + +**Solution**: Configure HTTPS with AWS Certificate Manager (see step 6 above) + +#### Architecture Notes + +- **nginx**: Serves frontend static files, proxies API and Socket.IO to backend +- **backend**: Node.js/Express container on port 5000 +- **Load Balancer**: Routes traffic with sticky sessions for Socket.IO +- **MongoDB**: External (MongoDB Atlas recommended) +- **Cloudinary**: External for image/audio storage + +#### Health Checks + +Load balancer checks path `/` on port 80 (nginx). For better monitoring, add health endpoint to backend: + +```javascript +// backend/server.js +app.get('/health', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); +``` + +Then configure load balancer health check path to `/api/health`. + +#### Monitoring + +```bash +# Check environment status +eb health + +# View logs +eb logs --stream + +# Check running containers +eb ssh +sudo docker ps +sudo docker logs waffle-chat-backend +``` + +### Alternative: Traditional Deployment + +#### Backend 1. Set environment variables 2. Ensure MongoDB is accessible 3. Run `npm start` or use PM2 -### Frontend +#### Frontend 1. Build: `npm run build` 2. Serve dist/ folder with nginx/vercel/netlify diff --git a/frontend/src/services/socket.ts b/frontend/src/services/socket.ts index a91e363..d22fa61 100644 --- a/frontend/src/services/socket.ts +++ b/frontend/src/services/socket.ts @@ -1,7 +1,7 @@ import { io, Socket } from 'socket.io-client'; import type { Message } from '../types'; -const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || 'http://localhost:5000'; +const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || window.location.origin; class SocketService { private socket: Socket | null = null; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 51021e4..20841c7 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; // https://vite.dev/config/ export default defineConfig({ diff --git a/nginx.conf b/nginx.conf index 45401fc..db95f0c 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,30 +12,24 @@ http { index index.html; # --- CRITICAL FIX: Allow blob: URLs and the specific extension ID in scripts --- - set $CSP_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: chrome-extension://*/ ; "; + set $CSP_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: chrome-extension://*"; # --- Styles: Necessary for modern JS frameworks (React/Vite/Tailwind) --- - set $CSP_style "style-src 'self' 'unsafe-inline' *.googleapis.com; "; + set $CSP_style "style-src 'self' 'unsafe-inline' *.googleapis.com"; # --- Images: Allow self, data URIs, and Cloudinary for user uploads --- - set $CSP_image "img-src 'self' data: https://res.cloudinary.com; "; + set $CSP_image "img-src 'self' data: blob: https://res.cloudinary.com"; + # --- Media: Allow blob and data URIs for audio/video previews --- + set $CSP_media "media-src 'self' blob: data: https://res.cloudinary.com"; # --- Connect: CRUCIAL for Socket.IO WebSockets (ws/wss) --- - # Allows connections back to the host via all protocols. - set $CSP_connect "connect-src 'self' ws://\$host wss://\$host; "; + # Allows connections back to the host via host variable + set $CSP_connect "connect-src 'self' ws: wss:"; # --- Fonts: Allow self and common CDNs --- - set $CSP_font "font-src 'self' data: *.googleapis.com *.gstatic.com; "; + set $CSP_font "font-src 'self' data: *.googleapis.com *.gstatic.com"; # --- Other directives (kept restrictive) --- - set $CSP_frame "frame-src 'self'; "; - set $CSP_object "object-src 'self'; "; - set $CSP_base "base-uri 'self'; "; + set $CSP_frame "frame-src 'self'"; + set $CSP_object "object-src 'self'"; + set $CSP_base "base-uri 'self'"; # --- Assemble the Final CSP Header --- - set $CSP "default-src 'self' ; \ - ${CSP_script} \ - ${CSP_style} \ - ${CSP_image} \ - ${CSP_connect} \ - ${CSP_font} \ - ${CSP_frame} \ - ${CSP_object} \ - ${CSP_base}"; + set $CSP "default-src 'self'; ${CSP_script}; ${CSP_style}; ${CSP_image}; ${CSP_connect}; ${CSP_font}; ${CSP_frame}; ${CSP_object}; ${CSP_base}"; add_header Content-Security-Policy $CSP; location / { @@ -52,13 +46,13 @@ http { } # proxy websocket traffic for socket.io - # location /socket.io/ { - # proxy_pass http://backend:5000/socket.io/; - # proxy_http_version 1.1; - # proxy_set_header Upgrade $http_upgrade; - # proxy_set_header Connection "Upgrade"; - # proxy_set_header Host $host; - # proxy_cache_bypass $http_upgrade; - # } + location /socket.io/ { + proxy_pass http://backend:5000/socket.io/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } } }