diff --git a/backend/compose.yaml b/backend/compose.yaml index a9253e8..b1b03cc 100644 --- a/backend/compose.yaml +++ b/backend/compose.yaml @@ -55,8 +55,8 @@ services: target: production container_name: ctf-backend restart: unless-stopped - ports: - - "${PORT:-3000}:3000" + # ports: + # - "${PORT:-3000}:3000" volumes: - ./src/utils/ca.pem:/app/dist/utils/key.pem:ro env_file: @@ -68,7 +68,18 @@ services: condition: service_started mongo: condition: service_healthy - + nginx: + image: nginx:alpine + container_name: ctf-nginx + restart: unless-stopped + ports: + - "3000:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + networks: + - ctf-network + depends_on: + - app networks: ctf-network: diff --git a/backend/dockerfile b/backend/dockerfile index 124e0a3..23c428d 100644 --- a/backend/dockerfile +++ b/backend/dockerfile @@ -37,7 +37,7 @@ RUN npm ci --omit=dev && \ # Copy built files from builder stage COPY --from=builder /app/dist ./dist - +#Copy tls certificate COPY src/utils/ca.pem src/utils/cert.pem ./dist/utils/ # Change ownership to non-root user diff --git a/backend/nginx.conf b/backend/nginx.conf index dd12cd3..321e104 100644 --- a/backend/nginx.conf +++ b/backend/nginx.conf @@ -1,19 +1,26 @@ events { # Configuration for connection processing - worker_connections 1024; + worker_connections 1024; } http { include mime.types; upstream backendserver { - server 127.0.0.1:3000; + server ctf-backend:3000; } server { listen 80; # Listen on standard web port - server_name localhost; + server_name _; location / { + # Allow Cors Headers for Frontend + add_header 'Access-Control-Allow-Origin' 'http://localhost:5173' always; + # Allow specific methods + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE' always; + + # 3. Allow specific headers (like Content-Type or Authorization) + add_header 'Access-Control-Allow-Headers' 'X-Requested-With,Content-Type,Authorization' always; proxy_pass http://backendserver/; # Important headers to keep the user's info intact proxy_set_header Host $host; diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..8332437 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,69 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build outputs +dist +build +.vite + +# Environment files +.env +.env.* +!.env.example + +# Git +.git +.gitignore +.gitattributes + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Testing +coverage +.nyc_output +*.test.ts +*.test.tsx +*.spec.ts +*.spec.tsx + +# Documentation +README.md +docs +*.md + +# CI/CD +.github +.gitlab-ci.yml +.travis.yml +.circleci + +# Docker +Dockerfile +.dockerignore +docker-compose*.yml +compose.yaml + +# Misc +scripts +*.log +.cache + +# TypeScript +*.tsbuildinfo + +# ESLint +.eslintcache + diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..f3bcfa7 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,67 @@ +# ============================================ +# Build Stage +# ============================================ +FROM node:20-alpine AS builder + +# Set working directory +WORKDIR /app + +# Install security updates and required packages +RUN apk update && \ + apk upgrade && \ + apk add --no-cache libc6-compat && \ + rm -rf /var/cache/apk/* + +# Copy package files +COPY package.json package-lock.json ./ + +# Install dependencies with clean cache +RUN npm ci --only=production=false && \ + npm cache clean --force + +# Copy source code and configuration files +COPY . . + +# Build the application +RUN npm run build + +# ============================================ +# Production Stage +# ============================================ +FROM nginx:1.27-alpine AS production + +# Install security updates +RUN apk update && \ + apk upgrade && \ + apk add --no-cache curl && \ + rm -rf /var/cache/apk/* + +# Remove default nginx website +RUN rm -rf /usr/share/nginx/html/* + +# Copy built files from builder stage +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Create necessary directories and set permissions +# Nginx runs as root initially to bind to port 80, then drops to nginx user +RUN mkdir -p /var/cache/nginx /var/log/nginx /etc/nginx/conf.d && \ + chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chmod -R 755 /usr/share/nginx/html && \ + chmod -R 755 /var/cache/nginx && \ + chmod -R 755 /var/log/nginx + +# Expose port 80 +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..f2791dd --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,76 @@ +server { + listen 0.0.0.0:80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + # Content Security Policy + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' + 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src + 'self' data: https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' + http://localhost:3000; frame-ancestors 'self';" always; + + # Hide nginx version + server_tokens off; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml; + + # Cache static assets + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + # API proxy to backend + location /api/ { + proxy_pass http://127.0.0.1:3000/api/; + 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_cache_bypass $http_upgrade; + } + + # Handle React Router (SPA routing) + location / { + try_files $uri $uri/ /index.html; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Deny access to backup files + location ~ ~$ { + deny all; + access_log off; + log_not_found off; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 16e9ae1..5b0a0b3 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,7 +1,6 @@ import axios from 'axios'; -const API_BASE_URL = - import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:80/api'; const api = axios.create({ baseURL: API_BASE_URL,